# -*- coding: utf-8 -*- import cairo import math class DynamicFlyer: """Producer of a nice graphic.""" def __init__(self, square): """Prepare the rendering of statistics.""" self._square = square self._width = square * 2 self._height = square * 3 + 130 self._surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self._width, self._height) self._cr = cairo.Context(self._surface) self._cr.select_font_face('Verdana', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) self._backcolor = (1.0, 1.0, 1.0) self._forecolor = (0.0, 0.0, 0.0) self._cr.rectangle(0, 0, self._width, self._height) self._cr.set_source_rgba(*self._backcolor, 1.0) self._cr.fill() self._caption_sep = 15 self._caption_width = 15 self._caption_space = 6 self._current_left = 0 self._current_top = 0 self._current_height = 0 self._next_top = 0 def _book_room(self, width, height): """Reserve space for drawings.""" if (self._current_left + width) > self._width: self._current_left = 0 self._current_top = self._next_top self._current_height = height self._next_top = self._current_top + height else: if height > self._current_height: self._next_top += (height - self._current_height) self._current_height = height self._cr.save() self._cr.translate(self._current_left, self._current_top) self._current_left += width def print_header(self, text): """Display a main caption.""" self._cr.set_font_size(26) fascent, fdescent, fheight, fxadvance, fyadvance = self._cr.font_extents() x_off, y_off, tw, th = self._cr.text_extents(text)[:4] self._book_room(self._width, fheight + 10) nx = -tw / 2.0 ny = fheight self._cr.save() self._cr.translate(self._width / 2, 0) self._cr.translate(nx, ny) self._cr.move_to(0, 0) self._cr.set_source_rgba(*self._forecolor, 1.0) self._cr.show_text(text) self._cr.restore() def render_commit_timeline(self, start, end, data, created, color): """Render a chart for all commits.""" self._book_room(self._width, self._square) hmargin = self._width * 0.1 vmargin = self._square * 0.1 width = self._width - 2 * hmargin - 10 height = self._square - 2 * vmargin - 10 self._cr.save() self._cr.translate(hmargin, vmargin) # Find the max and normalize min_vol = min(data.values()) max_vol = max(data.values()) diff_vol = max_vol - min_vol sx = width / (end - start) sy = height / diff_vol # Display volumes self._cr.save() self._cr.scale(sx, sy) first = min(data.keys()) if created: self._cr.move_to(first - start, diff_vol) else: self._cr.move_to(0, diff_vol) self._cr.line_to(0, max_vol - data[first]) for time in sorted(data.keys()): vol = data[time] self._cr.line_to(time - start, max_vol - vol) last = max(data.keys()) self._cr.line_to(last - start, diff_vol) self._cr.close_path() self._cr.set_source_rgba(*color, 0.5) self._cr.fill_preserve() self._cr.set_source_rgba(*color, 1.0) self._cr.set_line_width(1 / (sx if sx > sy else sy)) self._cr.stroke() self._cr.restore() # Display the borders self._cr.move_to(0, 0) self._cr.line_to(0, height) self._cr.line_to(width, height) self._cr.set_source_rgba(*self._forecolor, 0.7) self._cr.stroke() self._cr.move_to(0, -10) self._cr.line_to(-4, 0) self._cr.line_to(4, 0) self._cr.set_source_rgba(*self._forecolor, 1.0) self._cr.fill() self._cr.move_to(width + 10, height) self._cr.line_to(width, height - 4) self._cr.line_to(width, height + 4) self._cr.set_source_rgba(*self._forecolor, 1.0) self._cr.fill() # Display some time marks unit = width / 12 for i in range(11): self._cr.move_to((i + 1) * unit, height - 2) self._cr.line_to((i + 1) * unit, height + 2) self._cr.set_source_rgba(*self._forecolor, 1.0) self._cr.stroke() # Display commits self._cr.save() self._cr.scale(sx, sy) for time, vol in data.items(): self._cr.rectangle(time - start - 2 / sx, max_vol - vol - 2 / sy, 4 / sx, 4 / sy) self._cr.set_source_rgba(*color, 1.0) self._cr.fill() self._cr.restore() # Print a caption as explaination self._cr.restore() count = len(data.values()) unit = '' if min_vol > 1000 or max_vol > 1000: min_vol /= 1000 max_vol /= 1000 unit = 'k' if min_vol > 1000 or max_vol > 1000: min_vol /= 1000 max_vol /= 1000 unit = 'M' caption = '%u commit%s - min: %u %sSLOC - max: %u %sSLOC' \ % (count, 's' if count > 1 else '', min_vol, unit, max_vol, unit) self._cr.set_font_size(16) fascent, fdescent, fheight, fxadvance, fyadvance = self._cr.font_extents() x_off, y_off, tw, th = self._cr.text_extents(caption)[:4] nx = -tw / 2.0 ny = fheight self._cr.save() self._cr.translate(self._width / 2, height + 2 * vmargin) self._cr.translate(nx, ny) self._cr.move_to(0, 0) self._cr.set_source_rgba(*self._forecolor, 1.0) self._cr.show_text(caption) self._cr.restore() def _render_pie(self, title, data): """Render a pie chart.""" cx = self._square / 2 cy = (self._square / 2) * 0.8 radius = self._square / 4 # Processing data stats = [] count = 0 total = 0 for k, v in data.items(): val, color = v stats.append((val, k, color)) count += 1 total += val stats = sorted(stats) # Drawing results done = 0 init_angle = - (stats[0][0] * 2 * math.pi) / (total * 2) last_angle = init_angle for stat in stats: value, _, color = stat if value > 0: angle1 = last_angle if (done + 1) == count: angle2 = init_angle if init_angle != 0 else 2 * math.pi else: angle2 = angle1 + (value * 2 * math.pi) / total self._cr.move_to(cx, cy) self._cr.arc(cx, cy, radius, angle1, angle2) self._cr.line_to(cx, cy) self._cr.set_source_rgba(*color, 0.5) self._cr.fill_preserve() self._cr.set_source_rgba(*self._backcolor, 1.0) self._cr.set_line_width(2) self._cr.stroke() last_angle = angle2 done += 1 done = 0 last_angle = init_angle for stat in stats: value, _, color = stat if value > 0: angle1 = last_angle if (done + 1) == count: angle2 = init_angle if init_angle != 0 else 2 * math.pi else: angle2 = angle1 + (value * 2 * math.pi) / total self._cr.arc(cx, cy, radius, angle1, angle2) self._cr.set_source_rgba(*color, 1.0) self._cr.set_line_width(10) self._cr.stroke() last_angle = angle2 done += 1 # Printing the title py = cy + radius * 1.4 self._cr.set_font_size(24) fascent, fdescent, fheight, fxadvance, fyadvance = self._cr.font_extents() x_off, y_off, tw, th = self._cr.text_extents(title)[:4] nx = -tw / 2.0 ny = fheight / 2 self._cr.save() self._cr.translate(cx, py) self._cr.translate(nx, ny) self._cr.move_to(0, 0) self._cr.set_source_rgba(*self._forecolor, 1.0) self._cr.show_text(title) self._cr.restore() py += fheight # Printing the related captions full_width = 0 self._cr.set_font_size(16) for stat in stats: _, name, _ = stat x_off, y_off, tw, th = self._cr.text_extents(name)[:4] if full_width > 0: full_width += self._caption_sep full_width += self._caption_width + self._caption_space full_width += tw fascent, fdescent, fheight, fxadvance, fyadvance = self._cr.font_extents() nx = -full_width / 2.0 ny = fheight / 2 done = 0 start = 0 for stat in stats: _, name, color = stat x_off, y_off, tw, th = self._cr.text_extents(name)[:4] self._cr.save() self._cr.translate(cx, py) self._cr.translate(nx + start, ny) # Color top = -fheight + fdescent + (fheight - self._caption_width / 2) / 2 self._cr.rectangle(0, top, self._caption_width, self._caption_width / 2) self._cr.set_source_rgba(*color, 0.7) self._cr.fill_preserve() self._cr.set_source_rgba(*color, 1.0) self._cr.set_line_width(1) self._cr.stroke() # Text self._cr.move_to(self._caption_width + self._caption_space, 0) self._cr.set_source_rgba(*self._forecolor, 1.0) self._cr.show_text(name) self._cr.restore() start += self._caption_width + self._caption_space + tw + self._caption_sep done += 1 def render_pie(self, title, data): """Render a pie chart.""" self._book_room(self._square, self._square) self._render_pie(title, data) self._cr.restore() def print_footer(self, text): """Display an ending caption.""" self._cr.set_font_size(24) fascent, fdescent, fheight, fxadvance, fyadvance = self._cr.font_extents() x_off, y_off, tw, th = self._cr.text_extents(text)[:4] nx = -tw / 2.0 ny = fheight self._cr.save() self._cr.translate(self._width / 2, self._height - 1.8 * fheight) self._cr.translate(nx, 0) self._cr.move_to(0, 0) self._cr.set_source_rgba(*self._forecolor, 1.0) self._cr.show_text(text) self._cr.restore() def save(self, filename): """Store all the renderings into a PNG file.""" self._surface.write_to_png(filename)