diff options
Diffstat (limited to 'deluge/plugins/Stats/deluge/plugins/stats/graph.py')
-rw-r--r-- | deluge/plugins/Stats/deluge/plugins/stats/graph.py | 281 |
1 files changed, 184 insertions, 97 deletions
diff --git a/deluge/plugins/Stats/deluge/plugins/stats/graph.py b/deluge/plugins/Stats/deluge/plugins/stats/graph.py index 7f6a0548e..b5a265501 100644 --- a/deluge/plugins/Stats/deluge/plugins/stats/graph.py +++ b/deluge/plugins/Stats/deluge/plugins/stats/graph.py @@ -1,6 +1,7 @@ # # graph.py # +# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net> # Copyright (C) 2008 Damien Churchill <damoxc@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) Marcos Pinto 2007 <markybob@gmail.com> @@ -21,7 +22,7 @@ # along with deluge. If not, write to: # The Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor -# Boston, MA 02110-1301, USA. +# Boston, MA 02110-1301, USA. # # In addition, as a special exception, the copyright holders give # permission to link the code of portions of this program with the OpenSSL @@ -32,23 +33,14 @@ # but you are not obligated to do so. If you do not wish to do so, delete # this exception statement from your version. If you delete this exception # statement from all source files in the program, then also delete it here. -# -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception """ port of old plugin by markybob. """ import time +import math import cairo -import logging +from deluge.log import LOG as log from deluge.ui.client import client black = (0, 0, 0) @@ -60,13 +52,18 @@ green = (0, 1.0, 0) blue = (0, 0, 1.0) orange = (1.0, 0.74, 0) -log = logging.getLogger(__name__) - def default_formatter(value): return str(value) +def size_formatter_scale(value): + scale = 1.0 + for i in range(0,3): + scale = scale * 1024.0 + if value / scale < 1024: + return scale + def change_opacity(color, opactiy): - """A method to assist in changing the opacity of a color in order to draw the + """A method to assist in changing the opactiy of a color inorder to draw the fills. """ color = list(color) @@ -83,6 +80,7 @@ class Graph: self.length = 150 self.stat_info = {} self.line_size = 2 + self.dash_length = [10] self.mean_selected = True self.legend_selected = True self.max_selected = True @@ -105,125 +103,198 @@ class Graph: def set_stats(self, stats): self.last_update = stats["_last_update"] - log.debug("Last update: %s" % self.last_update) del stats["_last_update"] + self.length = stats["_length"] + del stats["_length"] + self.interval = stats["_update_interval"] + del stats["_update_interval"] self.stats = stats + return + + # def set_config(self, config): + # self.length = config["length"] + # self.interval = config["update_interval"] - def set_config(self, config): - self.length = config["length"] - self.interval = config["update_interval"] + def set_interval(self, interval): + self.interval = interval def draw_to_context(self, context, width, height): self.ctx = context self.width, self.height = width, height - try: - self.draw_rect(white, 0, 0, self.width, self.height) - self.draw_x_axis() - self.draw_left_axis() - - if self.legend_selected: - self.draw_legend() - except cairo.Error, e: - log.exception(e) + self.draw_rect(white, 0, 0, self.width, self.height) + self.draw_graph() return self.ctx def draw(self, width, height): - self.width = width - self.height = height - - self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height) - self.ctx = cairo.Context(self.surface) - self.draw_rect(white, 0, 0, self.width, self.height) - self.draw_x_axis() - self.draw_left_axis() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + ctx = cairo.Context(surface) + self.draw_to_context(ctx, width, height) + return surface - if self.legend_selected: - self.draw_legend() - return self.surface - def draw_x_axis(self): - duration = float(self.length * self.interval) + def draw_x_axis(self, bounds): + (left, top, right, bottom) = bounds + duration = self.length * self.interval start = self.last_update - duration - ratio = (self.width - 40) / duration - seconds_to_minute = 60 - time.localtime(start)[5] + ratio = (right - left) / float(duration) + + if duration < 1800 * 10: + #try rounding to nearest 1min, 5mins, 10mins, 30mins + for step in [60, 300, 600, 1800]: + if duration / step < 10: + x_step = step + break + else: + # if there wasnt anything useful find a nice fitting hourly divisor + x_step = ((duration / 5) /3600 )* 3600 - for i in xrange(0, 5): - text = time.strftime('%H:%M', time.localtime(start + seconds_to_minute + (60*i))) - x = int(ratio * (seconds_to_minute + (60*i))) - self.draw_text(text, x + 46, self.height - 20) - x = x + 59.5 - self.draw_dotted_line(gray, x, 20, x, self.height - 20) + #this doesnt allow for dst and timezones... + seconds_to_step = math.ceil(start/float(x_step)) * x_step - start - y = self.height - 22.5 - self.draw_dotted_line(gray, 60, y, int(self.width), y) + for i in xrange(0, duration/x_step + 1): + text = time.strftime('%H:%M', time.localtime(start + seconds_to_step + i*x_step)) + # + 0.5 to allign x to nearest pixel + x = int(ratio * (seconds_to_step + i*x_step) + left) + 0.5 + self.draw_x_text(text, x, bottom) + self.draw_dotted_line(gray, x, top-0.5, x, bottom+0.5) - def draw_left_axis(self): + self.draw_line(gray, left, bottom+0.5, right, bottom+0.5) + + def draw_graph(self): + font_extents = self.ctx.font_extents() + x_axis_space = font_extents[2] + 2 + self.line_size / 2.0 + plot_height = self.height - x_axis_space + #lets say we need 2n-1*font height pixels to plot the y ticks + tick_limit = (plot_height / font_extents[3] )# / 2.0 + + max_value = 0 + for stat in self.stat_info: + if self.stat_info[stat]['axis'] == 'left': + try: + l_max = max(self.stats[stat]) + except ValueError: + l_max = 0 + if l_max > max_value: + max_value = l_max + if max_value < self.left_axis['min']: + max_value = self.left_axis['min'] + + y_ticks = self.intervalise(max_value, tick_limit) + max_value = y_ticks[-1] + #find the width of the y_ticks + y_tick_text = [self.left_axis['formatter'](tick) for tick in y_ticks] + def space_required(text): + te = self.ctx.text_extents(text) + return math.ceil(te[4] - te[0]) + y_tick_width = max((space_required(text) for text in y_tick_text)) + + top = font_extents[2] / 2.0 + #bounds(left, top, right, bottom) + bounds = (y_tick_width + 4, top + 2, self.width, self.height - x_axis_space) + + self.draw_x_axis(bounds) + self.draw_left_axis(bounds, y_ticks, y_tick_text) + + def intervalise(self, x, limit=None): + """Given a value x create an array of tick points to got with the graph + The number of ticks returned can be constrained by limit, minimum of 3 + """ + #Limit is the number of ticks which is 1 + the number of steps as we + #count the 0 tick in limit + if limit is not None: + if limit <3: + limit = 2 + else: + limit = limit -1 + scale = 1 + if 'formatter_scale' in self.left_axis: + scale = self.left_axis['formatter_scale'](x) + x = x / float(scale) + + #Find the largest power of 10 less than x + log = math.log10(x) + intbit = math.floor(log) + + interval = math.pow(10, intbit) + steps = int(math.ceil(x / interval)) + + if steps <= 1 and (limit is None or limit >= 10*steps): + interval = interval * 0.1 + steps = steps * 10 + elif steps <= 2 and (limit is None or limit >= 5*steps): + interval = interval * 0.2 + steps = steps * 5 + elif steps <=5 and (limit is None or limit >= 2*steps): + interval = interval * 0.5 + steps = steps * 2 + + if limit is not None and steps > limit: + multi = steps / float(limit) + if multi > 2: + interval = interval * 5 + else: + interval = interval * 2 + + intervals = [i * interval * scale for i in xrange(1+int(math.ceil(x/ interval)))] + return intervals + + def draw_left_axis(self, bounds, y_ticks, y_tick_text): + (left, top, right, bottom) = bounds stats = {} - max_values = [] for stat in self.stat_info: if self.stat_info[stat]['axis'] == 'left': stats[stat] = self.stat_info[stat] stats[stat]['values'] = self.stats[stat] stats[stat]['fill_color'] = change_opacity(stats[stat]['color'], 0.5) stats[stat]['color'] = change_opacity(stats[stat]['color'], 0.8) - stats[stat]['max_value'] = max(self.stats[stat]) - max_values.append(stats[stat]['max_value']) - if len(max_values) > 1: - max_value = max(*max_values) - else: - max_value = max_values[0] - if max_value < self.left_axis['min']: - max_value = self.left_axis['min'] - - height = self.height - self.line_size - 22 - #max_value = float(round(max_value, len(str(max_value)) * -1)) - max_value = float(max_value) + height = bottom - top + max_value = y_ticks[-1] ratio = height / max_value - for i in xrange(1, 6): - y = int(ratio * ((max_value / 5) * i)) - 0.5 - if i < 5: - self.draw_dotted_line(gray, 60, y, self.width, y) - text = self.left_axis['formatter']((max_value / 5) * (5 - i)) - self.draw_text(text, 0, y - 6) - self.draw_dotted_line(gray, 60.5, 20, 60.5, self.height - 20) + for i, y_val in enumerate(y_ticks): + y = int(bottom - y_val * ratio ) - 0.5 + if i != 0: + self.draw_dotted_line(gray, left, y, right, y) + self.draw_y_text(y_tick_text[i], left, y) + self.draw_line(gray, left, top, left, bottom) for stat, info in stats.iteritems(): - self.draw_value_poly(info['values'], info['color'], max_value) - self.draw_value_poly(info['values'], info['fill_color'], max_value, info['fill']) + if len(info['values']) > 0: + self.draw_value_poly(info['values'], info['color'], max_value, bounds) + self.draw_value_poly(info['values'], info['fill_color'], max_value, bounds, info['fill']) def draw_legend(self): pass - def trace_path(self, values, max_value): - height = self.height - 24 - width = self.width + + def trace_path(self, values, max_value, bounds): + (left, top, right, bottom) = bounds + ratio = (bottom - top) / max_value line_width = self.line_size self.ctx.set_line_width(line_width) - self.ctx.move_to(width, height) + self.ctx.move_to(right, bottom) - self.ctx.line_to(width, - int(height - ((height - 28) * values[0] / max_value))) + self.ctx.line_to(right, int(bottom - values[0] * ratio )) - x = width - step = (width - 60) / float(self.length) + x = right + step = (right - left) / float(self.length -1) for i, value in enumerate(values): if i == self.length - 1: - x = 62 - self.ctx.line_to(x, - int(height - 1 - ((height - 28) * value / max_value)) - ) + x = left + + self.ctx.line_to(x, int(bottom - value * ratio)) x -= step self.ctx.line_to( - int(width + 62 - (((len(values) - 1) * width) / (self.length - 1))), - height) + int(right - (len(values) - 1) * step), + bottom) self.ctx.close_path() - def draw_value_poly(self, values, color, max_value, fill=False): - self.trace_path(values, max_value) + + def draw_value_poly(self, values, color, max_value, bounds, fill=False): + self.trace_path(values, max_value, bounds) self.ctx.set_source_rgba(*color) if fill: @@ -231,9 +302,26 @@ class Graph: else: self.ctx.stroke() - def draw_text(self, text, x, y): - self.ctx.set_font_size(9) - self.ctx.move_to(x, y + 9) + def draw_x_text(self, text, x, y): + """Draws text below and horizontally centered about x,y""" + fe = self.ctx.font_extents() + te = self.ctx.text_extents(text) + height = fe[2] + x_bearing = te[0] + width = te[2] + self.ctx.move_to(int(x - width/2.0 + x_bearing), int(y + height)) + self.ctx.set_source_rgba(*self.black) + self.ctx.show_text(text) + + def draw_y_text(self, text, x, y): + """Draws text left of and vertically centered about x,y""" + fe = self.ctx.font_extents() + te = self.ctx.text_extents(text) + descent = fe[1] + ascent = fe[0] + x_bearing = te[0] + width = te[4] + self.ctx.move_to(int(x - width - x_bearing - 2), int(y + (ascent - descent)/2.0)) self.ctx.set_source_rgba(*self.black) self.ctx.show_text(text) @@ -252,13 +340,12 @@ class Graph: def draw_dotted_line(self, color, x1, y1, x2, y2): self.ctx.set_source_rgba(*color) self.ctx.set_line_width(1) + dash, offset = self.ctx.get_dash() + self.ctx.set_dash(self.dash_length, 0) self.ctx.move_to(x1, y1) self.ctx.line_to(x2, y2) - #self.ctx.stroke_preserve() - #self.ctx.set_source_rgba(*white) - #self.ctx.set_dash((1, 1), 4) self.ctx.stroke() - #self.ctx.set_dash((1, 1), 0) + self.ctx.set_dash(dash, offset) if __name__ == "__main__": import test |