Prof. Dr. Detlef Kreuz

Charts

Die meisten Menschen können Diagramme besser interpretieren als reine Zahlenkolonnen. Aus diesem Grund wird z.B. die Anzahl der schon erledigten Aufgaben gerne in Form eines Liniendiagramm visualisiert. In Vorgehensmodellen wie Scrum werden gerne Burn-down charts verwendet, mit denen die noch nicht fertiggestellten Aufgaben in Bezug auf das jeweilige Datum dargestellt werden.

Auch beim Agilen Studieren visualisiere ich den Lernfortschritt meiner Studierenden mit einem solchen Diagramm, inzwischen mit einem Burn-up chart. Dazu nutze ich bisher das Universalprototypingwerkzeug mit Namen "Microsoft Excel" oder "Libreoffice Calc". Sicher wissen die meisten, wie man mit Hilfe dieser Werkzeuge aus tabellarisch angeordneten Daten ein entsprechendes Liniendiagramm erstellt. Mein letztes Diagramm sah in etwa so aus:

Burn-up chart

Es ist etwas monoton, dieses Diagramm aus Calc/Excel als PNG-Datei zu extrahieren. Meistens nutze ich ein Screenshot-Werkzeug. Bei der Auswahl des Bildausschnitts muss ich immer etwas genauer zielen, manchmal benötige ich zwei, drei Anläufe bis der Bildausschnitt in Ordnung ist. Vorher muss ich allerdings die Zahlen erfassen und die Daten für die Zukunft hochrechnen. Dabei überschreibe ich entweder die alten Hochrechnungen oder muss die Calc/Excel-Dateien versionieren. Für mich als WFM ist das nicht befriedigend.

Vor Kurzem entdeckte ich pygal, eine Python-Bibliothek, mit der SVG-Graphiken erstellt werden können. SVG-Daten lassen sich einfach in HTML-Seiten einbetten. Eine halbe Stunde später hatte ich erste Resultate:

Gut, die Farben stimmen nicht überein, aber das ist nicht schlimm. Ebenso ist die Soll-Linie nicht mehr gestrichelt. Dafür ist das Diagramm nun ein wenig interaktiv: die Punkte zeigen die konkreten Werte an und bei Auswahl einer Gruppe wird die entsprechende Linie leicht hervorgehoben.

Eine Stunde später konnte das Programm meine bisher manuell durchgeführten Hochrechnungen selbständig berechnen. Jetzt müsste mir nur jemand sagen, wie die Diagramme für die verschiedenen Studienphasen mittels eines Reglers ausgewählt werden können und schon lässt sich die zeitliche Entwicklung der Hochrechnungen einfach nachvollziehen. Sollte ich einmal eine dedizierte Software fürs Agile Studieren entwickeln, wäre dieses Visualisierung eine passende Komponente. So muss ich die Daten noch manuell erfassen.

Zum Schluss möchte ich für die Neugierigen das entsprechende Python-Programm als Vorschau angeben. Ich werde es demnächst ordentlich lizensieren und über ein Versionskontrollsystem allgemein verfügbar machen.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import (
    division, absolute_import, print_function, unicode_literals)

"""
:copyright: (c) 2015 by Detlef Kreuz
:license: Apache 2.0, see LICENSE
"""

from datetime import datetime, timedelta

import pygal
import pygal.style

STUDENT_RESULTS = [
    ('Gruppe 1', [0, 2, 20, 29, 29]),
    ('Gruppe 2', [17, 28, 46, 61, 74]),
    ('Gruppe 3', [9, 25, 46, 69, 79]),
    ('Gruppe 4', [7, 16, 33, 48, 62]),
    ('Gruppe 5', [10, 28, 44, 55, 77]),
    ('Gruppe 6', [13, 24, 40, 54, 65]),
    ('Gruppe 7', [11, 24, 40, 55, 59]),
    ('Gruppe 8', [10, 20, 31, 31, 31]),
    ('Gruppe 9', [1, 22, 35, 48, 51]),
]
START_DATE = datetime(2015, 3, 30)
END_DATE = datetime(2015, 6, 29)
SPRINT_LENGTH = timedelta(14)
NUMBER_TOPICS = 107

REVIEW_DATES = []
last_review = START_DATE
while True:
    last_review = last_review + SPRINT_LENGTH
    if last_review > END_DATE:
        break
    REVIEW_DATES.append(last_review)
REVIEW_DATES.append(END_DATE)
SPRINT_LENGTHS = [
    (sprint_end - sprint_start).days
    for (sprint_start, sprint_end) in zip(
        [START_DATE] + REVIEW_DATES, REVIEW_DATES)]
SPRINT_DAYS = [(review - START_DATE).days for review in REVIEW_DATES]
DESIRED_RESULTS = [
    int(round(days * NUMBER_TOPICS / SPRINT_DAYS[-1])) for days in SPRINT_DAYS]

CHART_STYLE = pygal.style.Style(
    background='#FFFFFF',
    plot_background='#FFFFFF',
    foreground='rgba(0, 0, 0, 0.7)',
    foreground_light='rgba(0, 0, 0, 0.9)',
    foreground_dark='rgba(0, 0, 0, 0.5)',
    colors=(
        'black',
        '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
        '#7F7F00', '#7F007F', '#007F7F', '#7F0000', '#007F00', '#00007F',
    ))


def forecast_results(results):
    next_pos = len(results)
    last_pos = next_pos - 1
    if last_pos < 0:
        return []
    last_result = results[last_pos]
    factor = last_result / SPRINT_DAYS[last_pos]
    if last_pos > 1:
        last_factor = (
            last_result - results[last_pos - 1]) / SPRINT_LENGTHS[last_pos]
        factor = (factor + last_factor) / 2
    forecast = results[:]
    for sprint_length in SPRINT_LENGTHS[next_pos:]:
        last_result = int(round(last_result + sprint_length * factor))
        forecast.append(last_result)
    return forecast


def create_chart(sprint_number):
    chart = pygal.Line(style=CHART_STYLE)
    chart.title = 'Bearbeitete Themen, Phase #{}'.format(sprint_number)
    chart.x_labels = [
        d.strftime('%d.%m.%y') for d in [START_DATE] + REVIEW_DATES]
    chart.add('SOLL', [0] + DESIRED_RESULTS)
    for name, results in STUDENT_RESULTS:
        chart.add(name, [0] + forecast_results(results[:sprint_number]))
    return chart


for i in range(len(REVIEW_DATES) + 1):
    create_chart(i).render_to_file('as-{}.svg'.format(i))