From 2e2851dc13d73352530dd4495c7e05603b2e520d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 23:38:38 +0200 Subject: Adding upstream version 2.1.2~dev0+20240219. Signed-off-by: Daniel Baumann --- deluge/plugins/Stats/deluge_stats/graph.py | 343 +++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 deluge/plugins/Stats/deluge_stats/graph.py (limited to 'deluge/plugins/Stats/deluge_stats/graph.py') diff --git a/deluge/plugins/Stats/deluge_stats/graph.py b/deluge/plugins/Stats/deluge_stats/graph.py new file mode 100644 index 0000000..ddb8f54 --- /dev/null +++ b/deluge/plugins/Stats/deluge_stats/graph.py @@ -0,0 +1,343 @@ +# +# Copyright (C) 2009 Ian Martin +# Copyright (C) 2008 Damien Churchill +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007 Marcos Mobley +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +""" +port of old plugin by markybob. +""" + +import logging +import math +import time + +import gi + +gi.require_foreign('cairo') + +import cairo # isort:skip (gi checks required before import). + +log = logging.getLogger(__name__) + +black = (0, 0, 0) +gray = (0.75, 0.75, 0.75) +white = (1.0, 1.0, 1.0) +darkred = (0.65, 0, 0) +red = (1.0, 0, 0) +green = (0, 1.0, 0) +blue = (0, 0, 1.0) +orange = (1.0, 0.74, 0) + + +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 opactiy of a color inorder to draw the + fills. + """ + color = list(color) + if len(color) == 4: + color[3] = opactiy + else: + color.append(opactiy) + return tuple(color) + + +class Graph: + def __init__(self): + self.width = 100 + self.height = 100 + 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 + self.black = (0, 0, 0) + self.interval = 2 # 2 secs + self.text_bg = (255, 255, 255, 128) # prototyping + self.set_left_axis() + + def set_left_axis(self, **kargs): + self.left_axis = kargs + + def add_stat(self, stat, label='', axis='left', line=True, fill=True, color=None): + self.stat_info[stat] = { + 'axis': axis, + 'label': label, + 'line': line, + 'fill': fill, + 'color': color, + } + + def set_stats(self, stats): + self.last_update = stats['_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_interval(self, interval): + self.interval = interval + + def draw_to_context(self, ctx, width, height): + self.width, self.height = width, height + self.draw_rect(ctx, white, 0, 0, self.width, self.height) + self.draw_graph(ctx) + + def draw(self, width, height): + """Create surface with context for use in tests""" + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + ctx = cairo.Context(surface) + self.draw_to_context(ctx, width, height) + return surface + + def draw_x_axis(self, ctx, bounds): + (left, top, right, bottom) = bounds + duration = self.length * self.interval + start = self.last_update - duration + ratio = (right - left) / 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 + + # this doesnt allow for dst and timezones... + seconds_to_step = math.ceil(start / x_step) * x_step - start + + for i in range(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(ctx, text, x, bottom) + self.draw_dotted_line(ctx, gray, x, top - 0.5, x, bottom + 0.5) + + self.draw_line(ctx, gray, left, bottom + 0.5, right, bottom + 0.5) + + def draw_graph(self, ctx): + font_extents = ctx.font_extents() + x_axis_space = font_extents[2] + 2 + self.line_size / 2 + 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] + + 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(ctx, text): + te = ctx.text_extents(text) + return math.ceil(te[4] - te[0]) + + y_tick_width = max(space_required(ctx, text) for text in y_tick_text) + + top = font_extents[2] / 2 + # bounds(left, top, right, bottom) + bounds = (y_tick_width + 4, top + 2, self.width, self.height - x_axis_space) + + self.draw_x_axis(ctx, bounds) + self.draw_left_axis(ctx, 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 / scale + + # Find the largest power of 10 less than x + comm_log = math.log10(x) + intbit = math.floor(comm_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 / limit + if multi > 2: + interval = interval * 5 + else: + interval = interval * 2 + + intervals = [ + i * interval * scale for i in range(1 + int(math.ceil(x / interval))) + ] + return intervals + + def draw_left_axis(self, ctx, bounds, y_ticks, y_tick_text): + (left, top, right, bottom) = bounds + stats = {} + 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) + + height = bottom - top + max_value = y_ticks[-1] + ratio = height / max_value + + for i, y_val in enumerate(y_ticks): + y = int(bottom - y_val * ratio) - 0.5 + if i != 0: + self.draw_dotted_line(ctx, gray, left, y, right, y) + self.draw_y_text(ctx, y_tick_text[i], left, y) + self.draw_line(ctx, gray, left, top, left, bottom) + + for stat, info in stats.items(): + if len(info['values']) > 0: + self.draw_value_poly( + ctx, info['values'], info['color'], max_value, bounds + ) + self.draw_value_poly( + ctx, + info['values'], + info['fill_color'], + max_value, + bounds, + info['fill'], + ) + + def draw_legend(self): + pass + + def trace_path(self, ctx, values, max_value, bounds): + (left, top, right, bottom) = bounds + ratio = (bottom - top) / max_value + line_width = self.line_size + + ctx.set_line_width(line_width) + ctx.move_to(right, bottom) + + ctx.line_to(right, int(bottom - values[0] * ratio)) + + x = right + step = (right - left) / (self.length - 1) + for i, value in enumerate(values): + if i == self.length - 1: + x = left + + ctx.line_to(x, int(bottom - value * ratio)) + x -= step + + ctx.line_to(int(right - (len(values) - 1) * step), bottom) + ctx.close_path() + + def draw_value_poly(self, ctx, values, color, max_value, bounds, fill=False): + self.trace_path(ctx, values, max_value, bounds) + ctx.set_source_rgba(*color) + + if fill: + ctx.fill() + else: + ctx.stroke() + + def draw_x_text(self, ctx, text, x, y): + """Draws text below and horizontally centered about x,y""" + fe = ctx.font_extents() + te = ctx.text_extents(text) + height = fe[2] + x_bearing = te[0] + width = te[2] + ctx.move_to(int(x - width / 2 + x_bearing), int(y + height)) + ctx.set_source_rgba(*self.black) + ctx.show_text(text) + + def draw_y_text(self, ctx, text, x, y): + """Draws text left of and vertically centered about x,y""" + fe = ctx.font_extents() + te = ctx.text_extents(text) + descent = fe[1] + ascent = fe[0] + x_bearing = te[0] + width = te[4] + ctx.move_to(int(x - width - x_bearing - 2), int(y + (ascent - descent) / 2)) + ctx.set_source_rgba(*self.black) + ctx.show_text(text) + + def draw_rect(self, ctx, color, x, y, height, width): + ctx.set_source_rgba(*color) + ctx.rectangle(x, y, height, width) + ctx.fill() + + def draw_line(self, ctx, color, x1, y1, x2, y2): + ctx.set_source_rgba(*color) + ctx.set_line_width(1) + ctx.move_to(x1, y1) + ctx.line_to(x2, y2) + ctx.stroke() + + def draw_dotted_line(self, ctx, color, x1, y1, x2, y2): + ctx.set_source_rgba(*color) + ctx.set_line_width(1) + dash, offset = ctx.get_dash() + ctx.set_dash(self.dash_length, 0) + ctx.move_to(x1, y1) + ctx.line_to(x2, y2) + ctx.stroke() + ctx.set_dash(dash, offset) -- cgit v1.2.3