def process_request(self, req): # initialize cache component self.cache = DataCache(self.env) # Grab arguments if re.match(r'/requirements/graph/\w+/?$',req.path_info) is not None: m = re.match(r'/requirements/graph/(\w+)/?$', req.path_info) (graph,) = m.groups() cacheddata = self.cache.get_data(None, None, None, graph) if cacheddata: data = pickle.loads(cacheddata) else: # Determine which image generator to call canvas = None # Initialize requirement (to grab metric data) self.requirement = Requirement(self.env) # Dashboard Graphs if graph[0:12] == 'dash_overall': canvas = self.dash_overall() elif graph[0:19] == 'component_req_count': canvas = self.component_req_count() elif graph[0:8] == 'dash_pie': canvas = self.dash_pie() # Other Graphs elif graph[0:7] == 'entropy': canvas = self.entropy_graph() # Extract the image into a string data = self._image_data(canvas) self.cache.set_data(None, None, None, graph, pickle.dumps(data)) else: m = re.match(r'/requirement/graph/(\w+)-(\w+)-(\w+)/(\w+)/?$', req.path_info) (component, fp, object, graph) = m.groups() cacheddata = self.cache.get_data(component, fp, object, graph) if cacheddata: data = pickle.loads(cacheddata) else: # Initialize requirement (to grab metric data) self.requirement = Requirement(self.env, component, Fp(self.env, name=fp)['id'], Object(self.env, name=object)['id']) # Determine which image generator to call # *** Requirement specific graphs *** canvas = None if graph[0:3] == 'mls': (mls, width) = graph.split('_') data = self.most_least_cited( int( width ) ) else: if graph == 'test': canvas = self.test() elif graph[0:17] == 'changes_over_time': ( garbage, garbage, garbage, secs_in_unit ) = graph.split('_') canvas = self.changes_over_time(int(secs_in_unit)) elif graph[0:17] == 'tickets_over_time': canvas = self.tickets_over_time() # Extract the image into a string data = self._image_data(canvas) self.cache.set_data(component, fp, object, graph, pickle.dumps(data)) # Send reponse headers & the image string req.send_header('content-type', 'image/png') req.end_headers() req.write(data) # Return nothing, as we have done all the work return None
class RequirementGraphComponent(Component): implements(IRequestHandler) # Don't let trac.web.main.process_request use a template on us use_template = False # Reserve room for a Requirement instance requirement = None # IRequestHandler methods def match_request(self, req): """ Match /requirement/graph/component-fp-object/graph_name """ if re.match(r'/requirement/graph/\w+-\w+-\w+/\w+/?$', req.path_info) is not None: return True elif re.match(r'/requirements/graph/\w+/?$', req.path_info) is not None: return True else: return False def process_request(self, req): # initialize cache component self.cache = DataCache(self.env) # Grab arguments if re.match(r'/requirements/graph/\w+/?$',req.path_info) is not None: m = re.match(r'/requirements/graph/(\w+)/?$', req.path_info) (graph,) = m.groups() cacheddata = self.cache.get_data(None, None, None, graph) if cacheddata: data = pickle.loads(cacheddata) else: # Determine which image generator to call canvas = None # Initialize requirement (to grab metric data) self.requirement = Requirement(self.env) # Dashboard Graphs if graph[0:12] == 'dash_overall': canvas = self.dash_overall() elif graph[0:19] == 'component_req_count': canvas = self.component_req_count() elif graph[0:8] == 'dash_pie': canvas = self.dash_pie() # Other Graphs elif graph[0:7] == 'entropy': canvas = self.entropy_graph() # Extract the image into a string data = self._image_data(canvas) self.cache.set_data(None, None, None, graph, pickle.dumps(data)) else: m = re.match(r'/requirement/graph/(\w+)-(\w+)-(\w+)/(\w+)/?$', req.path_info) (component, fp, object, graph) = m.groups() cacheddata = self.cache.get_data(component, fp, object, graph) if cacheddata: data = pickle.loads(cacheddata) else: # Initialize requirement (to grab metric data) self.requirement = Requirement(self.env, component, Fp(self.env, name=fp)['id'], Object(self.env, name=object)['id']) # Determine which image generator to call # *** Requirement specific graphs *** canvas = None if graph[0:3] == 'mls': (mls, width) = graph.split('_') data = self.most_least_cited( int( width ) ) else: if graph == 'test': canvas = self.test() elif graph[0:17] == 'changes_over_time': ( garbage, garbage, garbage, secs_in_unit ) = graph.split('_') canvas = self.changes_over_time(int(secs_in_unit)) elif graph[0:17] == 'tickets_over_time': canvas = self.tickets_over_time() # Extract the image into a string data = self._image_data(canvas) self.cache.set_data(component, fp, object, graph, pickle.dumps(data)) # Send reponse headers & the image string req.send_header('content-type', 'image/png') req.end_headers() req.write(data) # Return nothing, as we have done all the work return None def test(self): # Grab a blank, 100x100 image and the color palette fig = self._new_image((2,2)) canvas = FigureCanvasAgg(fig) plot = fig.add_subplot(111) # Return our image return canvas def entropy_graph(self): """Creates a graph of entropy over time for requirements This returns an image with each requirement's entropy over time shown over the course of the project. If a project is old, or there are a lot of requirements, this can be a time consuming process. """ # Image generation prelude. fig = self._new_image((4,4)) canvas = FigureCanvasAgg(fig) ax = fig.add_axes([0.1, 0.2, 0.85, 0.75]) # Set up our time range from oldest time to now/newest. metric = RequirementMetric(Requirement(self.env)) start_time = metric.model.get_timestamp_first_entity() end_time = metric.model.get_timestamp_latest_req_reqchange_milestone() current_time = int(time.time()) if current_time > end_time: end_time = current_time #create the list of timestamps and make labels. times = [] timestamp = end_time # We'll use two week increments for now to reduce # delay while loading. seconds_in_unit = (60 * 60 * 24 * 7 * 2) while timestamp > start_time: times.append(timestamp) timestamp -= seconds_in_unit times.reverse() labels = [time.strftime("%b %d '%y",time.gmtime(timestamp)) \ for timestamp in times] # Create the coordinates lists for graphing coords = {} i = 0 for timestamp in times: i = i + 1 if timestamp > current_time: break; entropy_data = metric.entropy(timestamp) if entropy_data: _,_,_, req_entropies, _, _ = entropy_data for key in req_entropies.keys(): if not coords.has_key(key): coords[key] = [(i, req_entropies[key])] else: coords[key].append((i, req_entropies[key])) biggest = [] # Now we can actually graph them all for req in coords.keys(): x = [x[0] for x in coords[req]] y = [y[1] for y in coords[req]] biggest.append(max(y)) ax.plot(x,y,'k-') # Add the milestone markers. milestones = Requirement(self.env).get_milestones() x = [] for (_, due) in milestones: if due == 0: continue x.append( float(due - times[0]) / float( seconds_in_unit)) height = int(float(max(biggest)) + 1.5) ax.vlines(x, 0, height,colors='r') # Label and shape the graph ax.set_xticks(range(0, len(times), 1)) ax.set_xticklabels(labels) ax.set_xlim(0, len(times)) setp(ax.get_xticklabels(),'rotation', 80, fontsize=6) ax.set_xlabel('Time in two-week increments', fontsize=6) ax.set_yticks(arange(0,height, 1)) ax.set_ylim(0, height) setp(ax.get_yticklabels(), fontsize=6) ax.set_ylabel("Entropy", fontsize=6) return canvas def tickets_over_time(self): fig = self._new_image((3,3)) canvas = FigureCanvasAgg(fig) when = now = int(time.time()) month=['Jan','Feb','Mar','Apr','May','Jun', 'Jul','Aug','Sep','Oct','Nov','Dec'] numMonth=datetime.date.today().month dates={} #rotates months to puts the current month #as the last month in the array # FIXME: there has to be a simpler/clearer/faster # way to do this. (modulus and offsets?) for j in range(0,12): dates[j+1]=month[j] current=dates[numMonth] while(month[11]!=current): tempMonth=month[0] for i in range(0,11): month[i]=month[i+1] month[11]=tempMonth valEnh=[0,0,0,0,0,0,0,0,0,0,0,0] valDef=[0,0,0,0,0,0,0,0,0,0,0,0] valTas=[0,0,0,0,0,0,0,0,0,0,0,0] for i in range(11, -1, -1): metrics = self.requirement.get_tickets_over_time_metrics(when) for metric in metrics: type, count = metric if type == 'task': valTas[i] = count elif type == 'defect': valDef[i] = count elif type == 'enhancement': valEnh[i] = count when = when - (3600 * 24 * 30) maxVal = max([max(valEnh), max(valDef), max(valTas)]) #the maximum value if maxVal == 0: # If there are no tickets ever associated, return blank # image instead of empty graph. return canvas # If we have usable data (tickets reference this requirement) # then we will graph them by month. ax=fig.add_subplot(111) num = 12 #the total number of values ind = arange(num) # the x locations for the groups if maxVal > 20: # sets how often the y-axis increments inc = maxVal / 10 else: inc=1 width=.2 # sets the width of the bars maxY = maxVal + 5 p1 = ax.bar(ind,valEnh, width=width, align='center',color='b') p2 = ax.bar(ind+width,valTas, width=width, align='center',color='g') p3 = ax.bar(ind+2*width,valDef, width=width, align='center',color='r') ax.set_xticks(ind+((3/2)*width)) ax.set_xticklabels(month) ax.set_xlim(-width,len(ind)) setp(ax.get_xticklabels(),'rotation', 45, fontsize=8) ax.set_yticks(arange(0,maxY,(inc))) ax.set_ylim(0,maxY) # Return return canvas def component_req_count(self): """Show graph of how many requirements each component has. This returns a canvas drawn with matplotlib's backend graphics tools for display on the requirements dashboard. This graph list all components so they can be compared by their number of associated (active) requirements. This is one way of describing a components relative 'size' and 'importance' in relation to the other components. """ cr_list = Requirement(self.env).get_component_req_count() count = len(cr_list) #heres a trick to cycle colors. Can add more base colors later. base_colors = ['b', 'g', 'r', 'c', 'm'] colors = [base_colors[x % 5] for x in range(count)] fig = self._new_image((5,4)) canvas = FigureCanvasAgg(fig) ax = fig.add_axes([0.08, 0.35, 0.9, 0.6]) #extract the labels and values from cr_list labels = [x[0] for x in cr_list] y = [y[1] for y in cr_list] #establish height of y axis if max(y) == 0: height = 5 elif max(y) % 5 == 0: height = max(y) else: height = max(y) + 5 - (max(y) % 5) #relative width of each bar width = 1 #scale vertically if height >= 20: inc = height/10 else: inc = 1 #position labels appropriately for horizontal scaling if count > 20: label_offset = 0 if count > 30: rotation = 80 else: label_offset = .5*width rotation = 60 #set the horizontal points x = arange(0,count * width, width) #draw the graph p1 = ax.bar(x, y, width=width, color=colors) #set the x axis labels ax.set_xticks(x + label_offset) ax.set_xticklabels(labels) ax.set_xlim(0,count) setp(ax.get_xticklabels(), 'rotation',rotation,fontsize=6) #set the y axis labels ax.set_yticks(arange(0,height + 1, (inc))) ax.set_ylim(0,height) #we're done! return canvas def changes_over_time(self, seconds_in_unit=60): fig = self._new_image((3,3)) canvas = FigureCanvasAgg(fig) (time_created, times_changed) = self.requirement.get_changes_metrics() ax = fig.add_subplot(111) # Obtain the timestamp of the first entity every created and treat # that as the projects start time since there is no available project # start time. proj_start_time = self.requirement.get_timestamp_first_entity() # Obtain the largest timestamp of either a milestone, requirement, # or a requirement change. most_recent_time = self.requirement.get_timestamp_latest_req_reqchange_milestone() # Obtain the current time. current_time = int(time.time()) if (current_time > most_recent_time): most_recent_time = current_time # Obtain all the milestones and their associated due dates in # seconds since epoch. milestones = self.requirement.get_milestones() tot_horiz_units = int((most_recent_time - proj_start_time) / seconds_in_unit) # Initialize base variables height=10 startY=0 startX=time_created # Obtain the amount of time between each change made to the # requirement, and place them in an array valid_change_offsets = [] prev_time = time_created for cur_time in times_changed: if (cur_time != prev_time): cur_horiz_unit_offset = int(((cur_time - prev_time) / seconds_in_unit)) valid_change_offsets.append(cur_horiz_unit_offset) prev_time = cur_time # Append the amount of time between the last change and # the current time to the array cur_horiz_unit_offset = int(((current_time - prev_time) / seconds_in_unit)) valid_change_offsets.append(cur_horiz_unit_offset) # Set the point where the requirement will begin graphing cur_x_pos = int((time_created - proj_start_time) / seconds_in_unit) height=1 startX=0 startY=0 tot_horiz_units = ((most_recent_time - proj_start_time) / seconds_in_unit) #setup a blank template for y-axis increments graphSegments=[] for unit in range(0,tot_horiz_units): graphSegments.append(0) # Graph the requirement for cur_x_offset in valid_change_offsets: ax.broken_barh([ (cur_x_pos, cur_x_offset)] , (startY, height), facecolors='blue') cur_x_pos += cur_x_offset startY += height # Graph the milestones for (garbage, milestone_due) in milestones: if milestone_due == 0: continue milestone_x_pos = int((milestone_due - proj_start_time) / seconds_in_unit) ax.axvline(milestone_x_pos, color='r') last_date=milestone_due if last_date == 0: last_date = current_time # Set the limits and ticks of the graph x_tick_lines=[] for i in range(0, (tot_horiz_units + 1)): x_tick_lines.append(i) ax.set_xticks(x_tick_lines) ax.set(xticklabels=[]) ax.set_yticks(arange(0,(startY+height),height)) # Return our image return canvas def most_least_cited( self, width ): """This is the only function using gd, so we handle it more directly for now. """ image = gd.image((10*width, 10)) image.filledRectangle((0,0),(10*width-1, 10), image.colorAllocate((255,0,0))) buffer = cStringIO.StringIO() image.writePng(buffer) data = buffer.getvalue() buffer.close() return data def dash_overall(self, seconds_in_unit=604800): fig = self._new_image((5,5)) canvas = FigureCanvasAgg(fig) ax = fig.add_subplot(111) # Obtain the timestamp of the first entity every created and treat # that as the projects start time since there is no available project # start time. proj_start_time = self.requirement.get_timestamp_first_entity() start_time = proj_start_time # Obtain the largest timestamp of either a milestone, requirement, # or a requirement change. most_recent_time = self.requirement.get_timestamp_latest_req_reqchange_milestone() # Obtain the current time. current_time = int(time.time()) if (current_time > most_recent_time): most_recent_time = current_time #This is the bargraph portion #Initialize variables width = 1 val = [] xTime = current_time endTime = proj_start_time #current_time - (31536000) #Get data while(xTime >= endTime): val.append(self.requirement.get_num_changes(endTime,endTime+seconds_in_unit)) endTime+=seconds_in_unit ind = arange(len(val)) # the x locations for the groups #Create bargraph graphBar = ax.bar(ind,val,width=width, color='y') #This is the line graph portion # this shows the number of active requirements #Initialize Variables #Get data #get the total number of requirements # at each given time checktime=proj_start_time yValues=[] while checktime <= current_time+seconds_in_unit: yValues.append(self.requirement.get_num_req(checktime)) checktime+=seconds_in_unit xValues=[] for _ in range(0,len(yValues)): xValues.append(_) #Create Line graph lineGraph=ax.plot(xValues,yValues) #create the list of timestamps and make labels. times = [] timestamp = current_time # We'll use two week increments for now to reduce # delay while loading. while timestamp > start_time: times.append(timestamp) timestamp -= seconds_in_unit times.reverse() labels = [time.strftime("%b %d '%y",time.gmtime(timestamp)) \ for timestamp in times] maxY=max(max(val),max(yValues)) if maxY < 20: maxY+=5 else: maxY=int(maxY*1.5) #This is the triangles portion # this is the validation points #Initialize Variables #Get data valTimes=[] valTimes=self.requirement.get_val_times() valHeight=[] temp=[] if len(valTimes) is not 0: for elem in valTimes: valHeight.append(maxY*0.9) temp.append(int((elem - proj_start_time) / seconds_in_unit)) valTimes=temp #Create triangles graph #use a scatterplot maybe and make each point for the graph be a triangle validationGraph=ax.scatter(valTimes,valHeight,color='g', marker='^', linewidth=5) #This is the milestones portion #Initialize Variables #Get data # Obtain all the milestones and their associated due dates in # seconds since epoch. milestones = self.requirement.get_milestones() tot_horiz_units = ((most_recent_time - proj_start_time) / seconds_in_unit) #Create line for milestones graph for (garbage, milestone_due) in milestones: if milestone_due == 0: continue milestone_x_pos = int((milestone_due - proj_start_time) / seconds_in_unit) #axvline(x=milestone_x_pos, xymin=0,ymax=100, color='r') milestoneBar = ax.bar(milestone_x_pos+0.5,maxY,width=(0), color='r', edgecolor='r') ax.set_xticklabels(labels) ax.set_xlim(0, len(times)) setp(ax.get_xticklabels(),'rotation', 80, fontsize=6) #format the graph #ax.set_xlim(proj_start_time,most_recent_time) ax.set_ylim(0,maxY) #return the graph return canvas def dash_pie(self): fig = self._new_image((4,4)) canvas = FigureCanvasAgg(fig) ax = fig.add_axes([0.1, 0.1, 0.8, 0.8]) fracs = [] vals = Requirement(self.env).get_type_req_tickets() total = 0 for val in vals: total += val labels = [] if total == 0: labels = ['None'] fracs = [1] else: if vals[0] != 0: labels.append('Open') fracs.append(float(vals[0]) / float(total)) if vals[1] != 0: labels.append('Closed') fracs.append(float(vals[1]) / float(total)) if vals[2] != 0: labels.append('None') fracs.append(float(vals[2]) / float(total)) labels = tuple(labels) ax.pie(fracs, labels=labels, autopct='%1.1f%%') return canvas def _new_image(self, (width,height)): # Return the blank figure return Figure(figsize=(width,height))