diff options
| author | Cyrille Bagard <nocbos@gmail.com> | 2018-01-14 20:40:23 (GMT) | 
|---|---|---|
| committer | Cyrille Bagard <nocbos@gmail.com> | 2018-01-14 20:40:23 (GMT) | 
| commit | 15b01266e1d7e193280658f300fd7d95bd918626 (patch) | |
| tree | 4bc1a034dd1d379b8eeda917feee5303800ae802 | |
Created an initial version with dummy rendering.
| -rw-r--r-- | board.py | 441 | ||||
| -rwxr-xr-x | chrysalide.sh | 10 | ||||
| -rw-r--r-- | gas.py | 78 | 
3 files changed, 529 insertions, 0 deletions
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 @@ -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)  | 
