def __init__(self, placeholder, data): Chart.__init__(self, placeholder) self.chart.type = 'pie' self.title.text = '' self.tooltip.enabled = False partnerData, resultData = [], [] self.series = [ {'name': 'Partners', 'data': partnerData, 'size': '60%', 'dataLabels': {'distance': 50}}, {'name': 'Results', 'data': resultData, 'size': '80%', 'innerSize': '60%', 'dataLabels': {'enabled': False}}, ] for partner in sorted(data): info = data[partner] clr = "#%02x%02x%02x" % getPlayerColor(partner) partnerData.append({ 'name': partner, 'y': info['games'], 'color': clr, }) resultData.append({ 'name': 'Wins', 'y': info['wins'], 'color': clr, }) resultData.append({ 'name': 'Losses', 'y': info['games'] - info['wins'], 'color': clr, })
def __init__(self, placeholder, data): Chart.__init__(self, placeholder) self.chart.type = 'column' self.title.text = '' self.tooltip.shared = True self.tooltip.formatter = raw("function() {return 'Made <b>' + this.points[1].y + '</b>/<b>' + this.points[0].y + '</b> when bidding <b>' + this.x + '</b>';}") self.plotOptions.column.grouping = False self.plotOptions.column.borderWidth = 0 with self.xAxis as axis: axis.title.text = 'Bid' axis.min = 0 axis.max = 13 axis.tickInterval = 1 axis.categories = ['Nil'] + range(1, 14) with self.yAxis as axis: axis.title.text = 'Times' axis.min = 0 axis.tickInterval = 1 bid, made = [], [] self.series = [ {'name': 'Bid', 'pointPadding': .3, 'data': bid}, {'name': 'Made', 'pointPadding': .4, 'data': made}, ] for i in range(14): bid.append(data[i]['count']) made.append(data[i]['made'])
def __init__(self, placeholder, round): Chart.__init__(self, placeholder) self.chart.type = 'column' self.title.text = '' self.xAxis.categories = ["Trick %d" % (i + 1) for i in range(13)] # self.yAxis = [ # {'title': {'text': 'Tricks'}, 'min': 0, 'max': 13, 'tickInterval': 1}, # {'title': {'text': 'Bids'}, 'opposite': True, 'min': 0, 'max': 13}, # ] with self.yAxis as axis: axis.title.text = 'Tricks' axis.min = 0 axis.max = 13 axis.tickInterval = 1 axis.plotLines = plotLines = [] clrs = ['red', 'green'] import sys for team in round.game.teams: plotLines.append({ 'label': {'text': annotatedTeamName(round.game, team)}, 'width': 2, 'color': clrs.pop(0), 'value': sum(bidValue(round.bidsByPlayer.get(player, 0)) for player in team), }) if plotLines[0]['value'] == plotLines[1]['value']: plotLines[0]['label']['text'] = 'Both teams' plotLines.pop(1) self.tooltip.enabled = False self.plotOptions.column.stacking = 'normal' # Not sure if there's a nice use for flags # Was planning to mark when players made their bid, but it doesn't look good and the team bids are obvious from the plotlines # flagSeries = { # 'type': 'flags', # 'data': [], # 'color': '#4572a7', # 'shape': 'flag', # 'onSeries': '', # 'showInLegend': False, # 'shape': 'squarepin', # } winners = [trick.winner if trick else None for trick in round.tricks] taken = {player: [0] for player in round.game.players} for winner in winners: for player, l in taken.iteritems(): l.append(l[-1] + (1 if player == winner else 0)) for l in taken.values(): l.pop(0) self.series = [{ 'name': player, 'stack': "Team %d" % (i%2 + 1), 'color': "#%02x%02x%02x" % getPlayerColor(player), 'data': taken[player], } for i, player in enumerate(round.game.players)]
def __init__(self, placeholder, sprint, tasks, **_): Chart.__init__(self, placeholder) start = tsToDate(sprint.start) self.title.text = '' self.tooltip.formatter = "function() {return '<b>' + this.series.name + '</b><br>' + this.point.name + ': '+ this.point.x + ' (' + this.y + '%)';}" self.credits.enabled = False self.series = seriesList = [] series = { 'type': 'pie', 'name': 'Start', 'size': '45%', 'innerSize': '20%', 'showInLegend': True, 'dataLabels': {'enabled': False}, 'data': [] } seriesList.append(series) originalTasks = filter(None, (task.getStartRevision(False) for task in tasks)) clrGen = cycle(colors) total = sum(t.effectiveHours() for t in originalTasks) for user in sorted(sprint.members): hours = sum(t.effectiveHours() for t in originalTasks if user in t.assigned) series['data'].append({ 'name': user.username, 'x': hours, 'y': float("%2.2f" % (100 * hours / total if total > 0 else 0)), 'color': clrGen.next() }) series = { 'type': 'pie', 'name': 'Now', 'innerSize': '45%', 'dataLabels': {'enabled': False}, 'data': [] } seriesList.append(series) clrGen = cycle(colors) total = sum(t.effectiveHours() for t in tasks) for user in sorted(sprint.members): hours = sum(t.effectiveHours() for t in tasks if user in t.assigned) series['data'].append({ 'name': user.username, 'x': hours, 'y': float("%2.2f" % (100 * hours / total if total > 0 else 0)), 'color': clrGen.next() })
def __init__(self, placeholder, sprint, tasks, revisions, **_): Chart.__init__(self, placeholder) days = [day for day in sprint.getDays()] now = Weekday.today() futureStarts = minOr(filter(lambda day: day > now, days), None) futureIndex = days.index(futureStarts) if futureStarts else None self.chart.type = 'area' self.title.text = '' self.plotOptions.area.stacking = 'percent' self.plotOptions.area.marker.enabled = False self.tooltip.shared = True self.credits.enabled = False with self.xAxis as xAxis: xAxis.tickmarkPlacement = 'on' xAxis.maxZoom = 1 xAxis.title.text = 'Day' xAxis.categories = [day.strftime('%a') for day in sprint.getDays()] # Future bar if futureIndex: xAxis.plotBands = [{ 'color': '#DDD', 'from': futureIndex - 0.75, 'to': len(days) - 0.5, # 'zIndex': 5 }] self.yAxis.min = 0 self.yAxis.title.text = 'Percentage of tasks' self.series = seriesList = [] counts = OrderedDict((name, []) for block in statusMenu for name in block) self.colors = [colorMap[type] for type in counts] for type, count in counts.iteritems(): seriesList.append({ 'name': statuses[type].text, 'data': count }) for day in days: tasksToday = [revisions[t.id, day] for t in tasks] for type, count in counts.iteritems(): count.append(len(filter(lambda task: task and task.status == type, tasksToday))) setupTimeline(self, sprint)
def __init__(self, placeholder, round): Chart.__init__(self, placeholder) self.chart.type = 'heatmap' self.title.text = '' self.legend.enabled = False self.xAxis.categories = round.game.players self.yAxis.categories = ['Spades', 'Diamonds', 'Clubs', 'Hearts'] self.yAxis.title = '' with self.colorAxis as axis: axis.min = 0 axis.max = 13. / 2 # I don't know why, but halving the max makes the axis come out right # We assume most people will have <= 5 cards in a suit, so we bunch the gradient around the lower numbers. #ff3f3f is 75% of the way to #ff0000 (HSV 0/.75/1) axis.stops = [[0, '#ffffff'], [5./13, '#ff3f3f'], [1, '#ff0000']] # player: ['As', '10c', ...] deal = round.deal data = [[xI, yI, sum(card[-1] == y[0].lower() for card in deal.get(x, []))] for xI, x in enumerate(self.xAxis.categories.get()) for yI, y in enumerate(self.yAxis.categories.get())] self.series = [{'data': data}]
def __init__(self, group): Chart.__init__(self, 'group-goals-chart') tasks = group.getTasks() goals = set(task.goal for task in tasks) self.title.text = '' self.plotOptions.pie.dataLabels.enabled = True self.tooltip.formatter = "function() {return this.point.name + ': '+ this.point.x + (this.point.x == 1 ? ' task' : ' tasks') + ' (' + this.y + '%)';}" self.credits.enabled = False self.series = [{ 'type': 'pie', 'name': 'Sprint Goals', 'data': [{ 'name': goal.name if goal else 'Other', 'color': goal.getHTMLColor() if goal else '#ccc', 'x': sum(task.goal == goal or False for task in tasks) } for goal in goals] }] # Percentages for m in self.series[0].data.get(): m['y'] = float("%2.2f" % (m['x'] * 100 / len(tasks)))
def __init__(self, placeholder, sprint, allTasks, revisions, **_): Chart.__init__(self, placeholder) days = [day for day in sprint.getDays()] now = Weekday.today() futureStarts = minOr(filter(lambda day: day > now, days), None) futureIndex = days.index(futureStarts) if futureStarts else None self.chart.defaultSeriesType = 'line' self.chart.zoomType = 'x' self.title.text = '' self.tooltip.shared = True self.credits.enabled = False with self.xAxis as xAxis: xAxis.tickmarkPlacement = 'on' xAxis.maxZoom = 1 xAxis.title.text = 'Day' # Future bar if futureIndex: xAxis.plotBands = [{ 'color': '#DDD', 'from': futureIndex - 0.75, 'to': len(days) - 0.5 }] self.yAxis.min = 0 self.yAxis.title.text = 'Hours' self.series = seriesList = [] for user in sorted(sprint.members): series = { 'name': user.username, 'data': [] } seriesList.append(series) for day in days: series['data'].append(sum(t.effectiveHours() if t and user in t.assigned and not t.deleted else 0 for t in [revisions[t.id, day] for t in allTasks])) setupTimeline(self, sprint)
def __init__(self, placeholder, game): Chart.__init__(self, placeholder) teams = game.teams # This is O(n log n) internally out of laziness; it could be O(n) if we calculated the round scores manually with round.scoreChange instead of round.score # We skip the active round as it has no score change (it will show as the same score as the previous round) scores = [{team: 0 for team in teams}] + [round.score for round in game.rounds if round.finished] self.chart.type = 'line' self.chart.marginTop = 30 self.title.text = '' self.tooltip.shared = True self.plotOptions.line.dataLabels.enabled = True self.xAxis.categories = [''] + ["Round %d" % (i + 1) for i in range(len(game.rounds))] self.yAxis.title.enabled = False self.yAxis.plotLines = [{ 'value': game.goal, 'color': '#0a0', 'width': 2, }] # Largest score seen or goal score, rounded up to next multiple of 100 self.yAxis.max = int(ceil(max(max(max(*score.values()) for score in scores), game.goal) / 100.)) * 100 # Show a line every 100 points, unless the score is gigantic; then just let highcharts choose automatically if game.goal <= 1000: self.yAxis.tickInterval = 100 flags = {team: [] for team in teams} flagSeries = [{ 'type': 'flags', 'data': data, 'color': '#4572a7', 'shape': 'flag', 'onSeries': '/'.join(team), 'showInLegend': False, 'shape': 'squarepin', 'y': -50, } for team, data in flags.iteritems()] for team in teams: bags = 0 for i, round in enumerate(game.rounds): if not round.finished: continue toShow = [] bids = [round.bidsByPlayer[player] for player in team] taken = [len(round.tricksByWinner[player]) for player in team] for player, bid, took in zip(team, bids, taken): if bid in ('nil', 'blind'): toShow.append(('N', "%s %s %s" % (player, 'made' if took == 0 else 'failed', 'nil' if bid == 'nil' else 'blind nil'))) thisBags = sum(taken) - sum(map(bidValue, bids)) if thisBags > 0: bags += thisBags if bags >= game.bagLimit: bags %= game.bagLimit toShow.append(('B', 'Bagged out')) if toShow: flags[team].append({'x': i + 1, 'title': ','.join(title for title, text in toShow), 'text': '<br>'.join(text for title, text in toShow)}) self.series = [{'id': '/'.join(team), 'name': game.teamNames[team], 'data': [score[team] for score in scores]} for team in teams] + flagSeries self.series[0]['color'] = '#a00'; self.series[1]['color'] = '#00a';
def __init__(self, placeholder, sprint, allTasks, revisions, **_): Chart.__init__(self, placeholder) days = [day for day in sprint.getDays()] now = Weekday.today() futureStarts = minOr(filter(lambda day: day > now, days), None) futureIndex = days.index(futureStarts) if futureStarts else None self.chart.defaultSeriesType = 'line' self.chart.zoomType = 'x' self.title.text = '' self.plotOptions.line.dataLabels.enabled = True self.tooltip.shared = True self.credits.enabled = False with self.xAxis as xAxis: xAxis.tickmarkPlacement = 'on' xAxis.maxZoom = 1 xAxis.title.text = 'Day' # Future bar if futureIndex is not None: xAxis.plotBands = [{ 'color': '#DDD', 'from': futureIndex - 0.75, 'to': len(days) - 0.5 }] self.yAxis.min = 0 self.yAxis.title.text = 'Hours' self.series = seriesList = [] taskSeries = { 'id': 'taskSeries', 'name': 'Tasking', 'data': [], 'color': '#4572a7', 'zIndex': 2 } seriesList.append(taskSeries) availSeries = { 'name': 'Availability', 'data': [], 'color': '#aa4643', 'zIndex': 2 } seriesList.append(availSeries) flagSeries = { 'type': 'flags', 'data': [], 'color': '#4572a7', 'shape': 'flag', 'onSeries': 'taskSeries', 'showInLegend': False, 'y': 16 } seriesList.append(flagSeries) if futureIndex == 0: futureIndex = 1 statusToday, hoursToday = None, None for day in days[:futureIndex]: tasksToday = [revisions[t.id, day] for t in allTasks] statusYesterday, hoursYesterday = statusToday, hoursToday statusToday = {t: t.status for t in tasksToday if t and not t.deleted} hoursToday = {t: t.manHours() for t in tasksToday if t and not t.deleted} taskSeries['data'].append(sum(hoursToday.values())) if hoursYesterday: hoursDiff = {t: hoursToday.get(t, 0) - hoursYesterday.get(t, 0) for t in hoursToday} largeChanges = [t for t, h in hoursDiff.iteritems() if abs(h) >= 16] if largeChanges: texts = [] for t in largeChanges: if t not in hoursYesterday: texts.append("<span style=\"color: #f00\">(New +%d)</span> %s" % (t.effectiveHours(), t.name)) elif hoursDiff[t] > 0: texts.append("<span style=\"color: #f00\">(+%d)</span> %s" % (hoursDiff[t], t.name)) else: if t.status in ('in progress', 'not started'): texts.append("<span style=\"color: #0a0\">(%d)</span> %s" % (hoursDiff[t], t.name)) elif t.status == 'complete': texts.append("<span style=\"color: #0a0\">(Complete %d)</span> %s" % (hoursDiff[t], t.name)) else: texts.append("<span style=\"color: #999\">(%s %d)</span> %s" % (statuses[t.status].getRevisionVerb(statusYesterday.get(t, 'not started')), hoursDiff[t], t.name)) flagSeries['data'].append({'x': days.index(day), 'title': alphabet[len(flagSeries['data']) % len(alphabet)], 'text': '<br>'.join(texts)}) avail = Availability(sprint) for day in days: availSeries['data'].append(avail.getAllForward(day)) setupTimeline(self, sprint, ['Projected tasking']) # Add commitment percentage to the axis label labels = self.xAxis.categories.get() for i in range(len(labels)): # For future percentages, use today's hours (i.e. don't use the projected hours) needed = taskSeries['data'][min(i, futureIndex - 1) if futureIndex else i][1] thisAvail = availSeries['data'][i][1] pcnt = "%d" % (needed * 100 / thisAvail) if thisAvail > 0 else "inf" labels[i] += "<br>%s%%" % pcnt self.xAxis.categories = labels self.xAxis.labels.formatter = "function() {return this.value.replace('inf', '\u221e');}" # Trendline data = self.series[0].data.get() dailyAvail = dict((day, avail.getAll(day)) for day in days) totalAvail = 0 for daysBack in range(1, (futureIndex or 0) + 1): midPoint = [futureIndex - daysBack, data[futureIndex - daysBack][1]] if dailyAvail[days[midPoint[0]]] > 0: daysBack = min(daysBack + 2, futureIndex) startPoint = [futureIndex - daysBack, data[futureIndex - daysBack][1]] totalAvail = sum(dailyAvail[day] for day in days[startPoint[0] : midPoint[0]]) break if totalAvail > 0 and startPoint[0] != midPoint[0]: slope = (midPoint[1] - startPoint[1]) / (midPoint[0] - startPoint[0]) slopePerAvail = slope * (midPoint[0] - startPoint[0]) / totalAvail points, total = [], midPoint[1] total = taskSeries['data'][futureIndex - 1][1] points.append([futureIndex - 1, total]) for i in range(futureIndex, len(days)): total += slopePerAvail * dailyAvail[days[i]] points.append([i, total]) seriesList.append({ 'name': 'Projected tasking', 'data': points, 'color': '#666', 'dataLabels': {'formatter': "function() {return (this.point.x == %d) ? parseInt(this.y, 10) : null;}" % (len(days) - 1)}, 'marker': {'symbol': 'circle'}, 'zIndex': 1 })
def __init__(self, placeholder, tasks): Chart.__init__(self, placeholder) many = isinstance(tasks, list) if not many: tasks = [tasks] if len(set(task.sprint for task in tasks)) > 1: raise Exception("All tasks must be in the same sprint") sprint = tasks[0].sprint if len(tasks) > 0 else None self.chart.defaultSeriesType = 'line' self.chart.zoomType = 'x' self.title.text = '' self.tooltip.shared = False # Non-shared tooltip doesn't support overlapping points, which is stupid, so for now # manually format the tooltip to include the hover point and any other overlapping points. self.tooltip.formatter = """function() { // Date Header var tooltip = '<small>' + Highcharts.dateFormat('%A, %B %e, %Y', this.x) + '</small><br>'; // Index of current X and value of Y at current X in hovered series var xIndex = this.series.xData.indexOf(this.point.x); var yRefValue = this.point.y; // Grab all series in chart var allSeries = this.series.chart.series; // For each series, check if its Y value at current X index is overlapping with the reference Y value for (var i = 0; i < allSeries.length; i++) { var yValue = allSeries[i].yData[xIndex]; // If there is overlap, add it to the tooltip along with the hovered value if (allSeries[i].visible && yValue == yRefValue) { // HACK: hiding meta-data (task ID) in the name and slicing it off before displaying. idx = allSeries[i].name.indexOf(':'); tooltip += '<span style=\"color: ' + allSeries[i].color + '\">' + allSeries[i].name.slice(idx + 1) + '</span>: ' + yValue + (yValue == 1 ? ' hour' : ' hours') + '<br>'; } } return tooltip; }""" self.plotOptions.line.dataLabels.enabled = not many self.plotOptions.line.dataLabels.x = -10 self.plotOptions.line.step = True self.legend.enabled = False self.credits.enabled = False with self.xAxis as x: x.type = 'datetime' x.dateTimeLabelFormats.day = '%a' x.tickInterval = 24 * 3600 * 1000 x.maxZoom = 24 * 3600 * 1000 if sprint: x.min = (sprint.start - 24*3600) * 1000 x.max = (sprint.end - 24*3600) * 1000 x.title.text = 'Day' with self.yAxis as y: y.min = 0 y.tickInterval = 4 y.minorTickInterval = 1 y.title.text = 'Hours' self.series = seriesList = [] for task in tasks: revs = task.getRevisions() series = { 'name': "%d:%s" % (task.id, task.name) if many else 'Hours', 'data': [], } if many: series['events'] = {'click': "function() {window.location = '/tasks/%d';}" % task.id} seriesList.append(series) hoursByDay = dict((tsStripHours(rev.timestamp), rev.effectiveHours()) for rev in revs) hoursByDay[tsStripHours(min(dateToTs(getNow()), task.historyEndsOn()))] = task.effectiveHours() series['data'] += [(utcToLocal(date) * 1000, hours) for (date, hours) in sorted(hoursByDay.items())]
def __init__(self, placeholder, sprint, tasks, revisions, **_): Chart.__init__(self, placeholder) days = [day for day in sprint.getDays()] now = Weekday.today() futureStarts = minOr(filter(lambda day: day > now, days), None) futureIndex = days.index(futureStarts) if futureStarts else None self.chart.type = 'waterfall' self.tooltip.enabled = False self.title.text = '' self.legend.enabled = False self.credits.enabled = False with self.plotOptions.series.dataLabels as labels: labels.enabled = True labels.formatter = """ function() { sum = 0; max_x = this.point.x; for(i in this.series.points) { point = this.series.points[i]; sum += point.y; if(point.x == max_x) { break; } } return sum; } """ # labels.color = '#fff' labels.verticalAlign = 'top' labels.y = -20 with self.xAxis as xAxis: xAxis.type = 'category' xAxis.tickmarkPlacement = 'on' xAxis.categories = [day.strftime('%a') for day in sprint.getDays()] # Future bar if futureIndex: xAxis.plotBands = [{ 'color': '#DDD', 'from': futureIndex - 0.75, 'to': len(days) - 0.5 }] self.yAxis.min = 0 self.yAxis.title.text = 'Hours' self.series = seriesList = [] series = { 'type': 'waterfall', 'name': 'Earned value', 'data': [], 'upColor': '#4572a7', 'color': '#aa4643' } seriesList.append(series) yesterdaySum = 0 for day in days: dayTasks = [revisions[t.id, day] for t in tasks] todaySum = sum(t.earnedValueHours() for t in dayTasks if t) series['data'].append(todaySum - yesterdaySum) yesterdaySum = todaySum setupTimeline(self, sprint)