コード例 #1
0
ファイル: Charts.py プロジェクト: mrozekma/spades
	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,
			})
コード例 #2
0
ファイル: Charts.py プロジェクト: mrozekma/spades
	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'])
コード例 #3
0
ファイル: Charts.py プロジェクト: mrozekma/spades
	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)]
コード例 #4
0
ファイル: SprintCharts.py プロジェクト: mrozekma/Sprint
	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()
			})
コード例 #5
0
ファイル: SprintCharts.py プロジェクト: mrozekma/Sprint
	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)
コード例 #6
0
ファイル: Charts.py プロジェクト: mrozekma/spades
	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}]
コード例 #7
0
ファイル: SprintCharts.py プロジェクト: mrozekma/Sprint
	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)))
コード例 #8
0
ファイル: SprintCharts.py プロジェクト: mrozekma/Sprint
	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)
コード例 #9
0
ファイル: Charts.py プロジェクト: mrozekma/spades
	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';
コード例 #10
0
ファイル: SprintCharts.py プロジェクト: mrozekma/Sprint
	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
			})
コード例 #11
0
ファイル: SprintCharts.py プロジェクト: mrozekma/Sprint
	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())]
コード例 #12
0
ファイル: SprintCharts.py プロジェクト: mrozekma/Sprint
	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)