|
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") |