summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCyrille Bagard <nocbos@gmail.com>2018-01-14 20:40:23 (GMT)
committerCyrille Bagard <nocbos@gmail.com>2018-01-14 20:40:23 (GMT)
commit15b01266e1d7e193280658f300fd7d95bd918626 (patch)
tree4bc1a034dd1d379b8eeda917feee5303800ae802
Created an initial version with dummy rendering.
-rw-r--r--board.py441
-rwxr-xr-xchrysalide.sh10
-rw-r--r--gas.py78
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
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)