~ubuntu-core-dev/merge-o-matic/trunk

78 by Scott James Remnant
stat graph generation
1
#!/usr/bin/env python
96 by Scott James Remnant
Ignore events that occur before the cut-off start date.
2
# -*- coding: utf-8 -*-
114 by Scott James Remnant
add the GPL 3 to everything
3
# stats-graphs.py - output stats graphs
4
#
5
# Copyright © 2008 Canonical Ltd.
6
# Author: Scott James Remnant <scott@ubuntu.com>.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of version 3 of the GNU General Public License as
10
# published by the Free Software Foundation.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
78 by Scott James Remnant
stat graph generation
19
20
import os
21
import logging
22
import calendar
23
import datetime
24
25
from pychart import *
26
27
from momlib import *
28
29
30
# Order of stats we pick out
31
ORDER = [ "unmodified", "needs-sync", "local",
32
          "repackaged", "modified", "needs-merge"  ]
33
34
# Labels used on the graph
35
LABELS = {
36
    "unmodified":  "Unmodified",
37
    "needs-sync":  "Needs Sync",
38
    "local":       "Local",
39
    "repackaged":  "Repackaged",
40
    "modified":    "Modified",
41
    "needs-merge": "Needs Merge",
42
    }
43
44
# Colours (fill styles) used for each stat
45
FILL_STYLES = {
46
    "unmodified":  fill_style.blue,
47
    "needs-sync":  fill_style.darkorchid,
48
    "local":       fill_style.aquamarine1,
49
    "repackaged":  fill_style.green,
50
    "modified":    fill_style.yellow,
51
    "needs-merge": fill_style.red,
52
    }
53
54
# Offsets of individual stats on the pie chart (for pulling out)
55
ARC_OFFSETS = {
56
    "unmodified":  10,
57
    "needs-sync":  10,
58
    "local":       0,
59
    "repackaged":  5,
60
    "needs-merge": 0,
61
    "modified":    0,
62
    }
63
64
65
def options(parser):
66
    parser.add_option("-d", "--distro", type="string", metavar="DISTRO",
67
                      default=OUR_DISTRO,
68
                      help="Distribution to generate stats for")
69
70
def main(options, args):
71
    distro = options.distro
72
96 by Scott James Remnant
Ignore events that occur before the cut-off start date.
73
    # Read from the stats file
78 by Scott James Remnant
stat graph generation
74
    stats = read_stats()
75
76
    # Initialise pychart
77
    theme.use_color = True
78
    theme.reinitialize()
79
80
    # Get the range of the trend chart
81
    today = datetime.date.today()
82
    start = trend_start(today)
96 by Scott James Remnant
Ignore events that occur before the cut-off start date.
83
    events = get_events(stats, start)
78 by Scott James Remnant
stat graph generation
84
85
    # Iterate the components and calculate the peaks over the last six
86
    # months, as well as the current stats
87
    for component in DISTROS[distro]["components"]:
88
        # Extract current and historical stats for this component
89
        current = get_current(stats[component])
90
        history = get_history(stats[component], start)
91
92
        pie_chart(component, current)
93
        range_chart(component, history, start, today, events)
94
95
96
def date_to_datetime(s):
97
    """Convert a date string into a datetime."""
98
    (year, mon, day) = [ int(x) for x in s.split("-", 2) ]
99
    return datetime.date(year, mon, day)
100
101
def date_to_ordinal(s):
102
    """Convert a date string into an ordinal."""
103
    return date_to_datetime(s).toordinal()
104
105
def ordinal_to_label(o):
106
    """Convert an ordinal into a chart label."""
107
    d = datetime.date.fromordinal(int(o))
108
    return d.strftime("/hL{}%b %y")
109
110
111
def trend_start(today):
112
    """Return the date from which to begin displaying the trend chart."""
98 by Scott James Remnant
make the trend graph 9 months
113
    if today.month > 9:
78 by Scott James Remnant
stat graph generation
114
        s_year = today.year
98 by Scott James Remnant
make the trend graph 9 months
115
        s_month = today.month - 9
78 by Scott James Remnant
stat graph generation
116
    else:
117
        s_year = today.year - 1
98 by Scott James Remnant
make the trend graph 9 months
118
        s_month = today.month + 3
78 by Scott James Remnant
stat graph generation
119
120
    s_day = min(calendar.monthrange(s_year, s_month)[1], today.day)
121
    start = datetime.date(s_year, s_month, s_day)
122
123
    return start
124
125
126
def read_stats():
127
    """Read the stats history file."""
128
    stats = {}
129
130
    stats_file = "%s/stats.txt" % ROOT
131
    stf = open(stats_file, "r");
132
    try:
133
        for line in stf:
134
            (date, time, component, info) = line.strip().split(" ", 3)
135
136
            if component not in stats:
137
                stats[component] = []
138
139
            stats[component].append([date, time, info])
140
    finally:
141
        stf.close()
142
143
    return stats
144
96 by Scott James Remnant
Ignore events that occur before the cut-off start date.
145
def get_events(stats, start):
78 by Scott James Remnant
stat graph generation
146
    """Get the list of interesting events."""
96 by Scott James Remnant
Ignore events that occur before the cut-off start date.
147
    events = []
148
    for date, time, info in stats["event"]:
149
        if date_to_datetime(date) >= start:
150
            events.append((date, info))
151
152
    return events
78 by Scott James Remnant
stat graph generation
153
154
def info_to_data(date, info):
155
    """Convert an optional date and information set into a data set."""
156
    data = []
157
    if date is not None:
158
        data.append(date)
159
160
    values = dict(p.split("=", 1) for p in info.split(" "))
161
    for key in ORDER:
162
        data.append(int(values[key]))
163
164
    return data
165
166
167
def get_current(stats):
168
    """Get the latest information."""
169
    (date, time, info) = stats[-1]
170
    return info
171
172
def get_history(stats, start):
173
    """Get historical information for each day since start."""
174
    values = {}
175
    for date, time, info in stats:
176
        if date_to_datetime(date) >= start:
177
            values[date] = info
178
179
    dates = values.keys()
180
    dates.sort()
181
182
    return [ (d, values[d]) for d in dates ]
183
184
185
def date_tics(min, max):
186
    """Return list of tics between the two ordinals."""
187
    intervals = []
188
    for tic in range(min, max+1):
189
        if datetime.date.fromordinal(tic).day == 1:
190
            intervals.append(tic)
191
192
    return intervals
193
194
def sources_intervals(max):
195
    """Return the standard and minimal interval for the sources axis."""
100 by Scott James Remnant
adjust intervals
196
    if max > 10000:
197
        return (10000, 2500)
198
    elif max > 1000:
78 by Scott James Remnant
stat graph generation
199
        return (1000, 250)
200
    elif max > 100:
201
        return (100, 25)
202
    elif max > 10:
203
        return (10, 2.5)
204
    else:
205
        return (1, None)
206
207
208
def pie_chart(component, current):
209
    """Output a pie chart for the given component and data."""
210
    data = zip([ LABELS[key] for key in ORDER ],
211
               info_to_data(None, current))
212
213
    filename = "%s/merges/%s-now.png" % (ROOT, component)
214
    c = canvas.init(filename, format="png")
215
    try:
216
        ar = area.T(size=(300,250), legend=None,
217
                    x_grid_style=None, y_grid_style=None)
218
219
        plot = pie_plot.T(data=data, arrow_style=arrow.a0, label_offset=25,
220
                          shadow=(2, -2, fill_style.gray50),
221
                          arc_offsets=[ ARC_OFFSETS[key] for key in ORDER ],
222
                          fill_styles=[ FILL_STYLES[key] for key in ORDER ])
223
        ar.add_plot(plot)
224
225
        ar.draw(c)
226
    finally:
227
        c.close()
228
229
def range_chart(component, history, start, today, events):
230
    """Output a range chart for the given component and data."""
231
    data = chart_data.transform(lambda x: [ date_to_ordinal(x[0]),
232
                                            sum(x[1:1]),
233
                                            sum(x[1:2]),
234
                                            sum(x[1:3]),
235
                                            sum(x[1:4]),
236
                                            sum(x[1:5]),
237
                                            sum(x[1:6]),
238
                                            sum(x[1:7]) ],
239
                                [ info_to_data(date, info)
240
                                  	for date, info in history ])
241
242
    (y_tic_interval, y_minor_tic_interval) = \
243
                     sources_intervals(max(d[-1] for d in data))
244
245
    filename = "%s/merges/%s-trend.png" % (ROOT, component)
246
    c = canvas.init(filename, format="png")
247
    try:
99 by Scott James Remnant
make trend wider to compensate
248
        ar = area.T(size=(450,225), legend=legend.T(),
78 by Scott James Remnant
stat graph generation
249
                    x_axis=axis.X(label="Date", format=ordinal_to_label,
250
                                  tic_interval=date_tics,
251
                                  tic_label_offset=(10,0)),
252
                    y_axis=axis.Y(label="Sources", format="%d",
253
                                  tic_interval=y_tic_interval,
254
                                  minor_tic_interval=y_minor_tic_interval,
255
                                  tic_label_offset=(-10,0),
256
                                  label_offset=(-10,0)),
257
                    x_range=(start.toordinal(), today.toordinal()))
258
259
        for idx, key in enumerate(ORDER):
260
            plot = range_plot.T(data=data, label=LABELS[key],
261
                                min_col=idx+1, max_col=idx+2,
262
                                fill_style=FILL_STYLES[key])
263
            ar.add_plot(plot)
264
87 by Scott James Remnant
allow multiple annotations on a graph
265
        ar.draw(c)
266
96 by Scott James Remnant
Ignore events that occur before the cut-off start date.
267
        levels = [ 0, 0, 0 ]
268
269
        for date, text in events:
78 by Scott James Remnant
stat graph generation
270
            xpos = ar.x_pos(date_to_ordinal(date))
271
            ypos = ar.loc[1] + ar.size[1]
272
96 by Scott James Remnant
Ignore events that occur before the cut-off start date.
273
            for level, bar in enumerate(levels):
274
                if bar < xpos:
275
                    width = int(font.text_width(text))
276
                    levels[level] = xpos + 25 + width
277
                    break
278
            else:
279
                continue
87 by Scott James Remnant
allow multiple annotations on a graph
280
96 by Scott James Remnant
Ignore events that occur before the cut-off start date.
281
            tb = text_box.T(loc=(xpos + 25, ypos + 45 - (20 * level)),
282
                            text=text)
78 by Scott James Remnant
stat graph generation
283
            tb.add_arrow((xpos, ypos))
284
            tb.draw()
285
87 by Scott James Remnant
allow multiple annotations on a graph
286
            c.line(line_style.black_dash2, xpos, ar.loc[1], xpos, ypos)
78 by Scott James Remnant
stat graph generation
287
    finally:
288
        c.close()
289
290
291
if __name__ == "__main__":
292
    run(main, options, usage="%prog",
293
        description="output stats graphs")