From 15b01266e1d7e193280658f300fd7d95bd918626 Mon Sep 17 00:00:00 2001 From: Cyrille Bagard Date: Sun, 14 Jan 2018 21:40:23 +0100 Subject: Created an initial version with dummy rendering. --- board.py | 441 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chrysalide.sh | 10 ++ gas.py | 78 +++++++++++ 3 files changed, 529 insertions(+) create mode 100644 board.py create mode 100755 chrysalide.sh create mode 100644 gas.py diff --git a/board.py b/board.py new file mode 100644 index 0000000..aeda474 --- /dev/null +++ b/board.py @@ -0,0 +1,441 @@ +# -*- 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 + height = self._square - 2 * vmargin + + self._cr.save() + + self._cr.translate(hmargin, vmargin) + + # Find the max and normalize + + max_vol = max(data.values()) + + sx = width / (end - start) + sy = height / max_vol + + self._cr.save() + + self._cr.scale(sx, sy) + + # Display volumes + + first = min(data.keys()) + + if created: + + self._cr.move_to(first, max_vol) + + else: + + self._cr.move_to(0, max_vol) + self._cr.line_to(0, max_vol - data[first]) + + for time in sorted(data.keys()): + + vol = data[time] + + self._cr.line_to(time, max_vol - vol) + + last = max(data.keys()) + + self._cr.line_to(last, max_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) + self._cr.stroke() + + # Display commits + + for time, vol in data.items(): + + self._cr.rectangle(time - 2 / sx, max_vol - vol - 2 / sy, 4 / sx, 4 / sy) + + self._cr.set_source_rgba(*color, 1.0) + self._cr.set_line_width(0.5) + self._cr.fill() + + 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, 0) + self._cr.line_to(-4, 10) + self._cr.line_to(4, 10) + + self._cr.set_source_rgba(*self._forecolor, 1.0) + self._cr.fill() + + self._cr.move_to(width, height) + self._cr.line_to(width - 10, height - 4) + self._cr.line_to(width - 10, height + 4) + + self._cr.set_source_rgba(*self._forecolor, 1.0) + self._cr.fill() + + self._cr.restore() + + # Print a caption as explaination + + count = len(data.values()) + min_vol = min(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, colors): + """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(): + + stats.append((v, k)) + + count += 1 + total += v + + stats = sorted(stats) + + # Drawing results + + done = 0 + + init_angle = - (stats[0][0] * math.pi) / (2 * total) + + last_angle = init_angle + + for stat in stats: + + angle1 = last_angle + + if (done + 1) == count: + angle2 = init_angle + + else: + angle2 = angle1 + (stat[0] * 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(*colors[done], 0.5) + self._cr.fill_preserve() + + self._cr.set_source_rgba(*self._backcolor, 1.0) + self._cr.set_line_width(2) + self._cr.stroke() + + done += 1 + last_angle = angle2 + + done = 0 + + last_angle = init_angle + + for stat in stats: + + angle1 = last_angle + + if (done + 1) == count: + angle2 = init_angle + + else: + angle2 = angle1 + (stat[0] * math.pi) / total + + self._cr.arc(cx, cy, radius, angle1, angle2) + + self._cr.set_source_rgba(*colors[done], 1.0) + self._cr.set_line_width(10) + self._cr.stroke() + + done += 1 + last_angle = angle2 + + # 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: + + x_off, y_off, tw, th = self._cr.text_extents(stat[1])[: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: + + x_off, y_off, tw, th = self._cr.text_extents(stat[1])[: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(*colors[done], 0.7) + self._cr.fill_preserve() + + self._cr.set_source_rgba(*colors[done], 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(stat[1]) + + self._cr.restore() + + start += self._caption_width + self._caption_space + tw + self._caption_sep + + done += 1 + + + def render_pie(self, title, data, colors): + """Render a pie chart.""" + + self._book_room(self._square, self._square) + + self._render_pie(title, data, colors) + + 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) diff --git a/chrysalide.sh b/chrysalide.sh new file mode 100755 index 0000000..b43716c --- /dev/null +++ b/chrysalide.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +OUTPUT="2017-2018.png" + +python3 ./gas.py \ + --header "Chrysalide's Git repository in 2017" \ + --footer "Get more info at chrysalide.re" \ + --output $OUTPUT + +eog $OUTPUT diff --git a/gas.py b/gas.py new file mode 100644 index 0000000..40509e8 --- /dev/null +++ b/gas.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +import argparse +from board import DynamicFlyer + + + + + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser() + + parser.add_argument('--header', help='Define the main title of the picture', type=str, required=True) + parser.add_argument('--footer', help='Define a conclusion for the picture', type=str, required=True) + parser.add_argument('--output', help='Set the output filename', type=str, required=True) + + args = parser.parse_args() + + + + flyer = DynamicFlyer(250) + + flyer.print_header(args.header) + + data = { + 100 : 100, + 200 : 123, + 400 : 700, + 600 : 650, + 900 : 900 + } + + color = (0.5, 0.2, 0.7) + + color = (0.047, 0.365, 0.533) + + flyer.render_commit_timeline(0, 1000, data, False, color) + + colors = [ (0.812, 0.176, 0.08), (0.04, 0.58, 0.255) ] + + colors = [ (0.643, 0.133, 0), (0, 0.459, 0.20) ] + + colors = [ (1.0, 0.353, 0.165), (0.118, 0.71, 0.392) ] + + data = { + 'Insertions' : 794, + 'Deletions' : 312 + } + + flyer.render_pie('Code', data, colors) + + data = { + 'Added' : 100, + 'Removed' : 13 + } + + flyer.render_pie('Files', data, colors) + + data = { + 'C' : 2489, + 'Python' : 631 + } + + flyer.render_pie('Languages', data, colors) + + data = { + 'Remaining' : 89, + 'Killed' : 31 + } + + flyer.render_pie('TODO / FIXME', data, colors) + + + flyer.print_footer(args.footer) + + flyer.save(args.output) -- cgit v0.11.2-87-g4458