# Copyright (C) 2013, 2014 Red Hat, Inc. # # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. from gi.repository import GObject from gi.repository import Gtk # pylint: disable=arguments-differ # Newer pylint can detect, but warns that overridden arguments are wrong BASECOLOR = Gtk.StyleContext().lookup_color("theme_base_color")[1] def rect_print(name, rect): # pragma: no cover # For debugging print("%s: height=%d, width=%d, x=%d, y=%d" % (name, rect.height, rect.width, rect.x, rect.y)) def _line_helper(cairo_ct, bottom_baseline, points, for_fill=False): last_was_zero = False last_point = None for index, (x, y) in enumerate(points): # If stats value == 0, we don't want to draw a line is_zero = bool(y == bottom_baseline) # If the line is for filling, alter the coords so that fill covers # the same area as the parent sparkline: fill is one pixel short # to not overwrite the spark line if for_fill: if index == 0: x -= 1 elif index == (len(points) - 1): x += 1 elif last_was_zero and is_zero: y += 1 if index == 0: cairo_ct.move_to(x, y) elif last_was_zero and is_zero and not for_fill: cairo_ct.move_to(x, y) else: cairo_ct.line_to(x, y) last_point = (x, y) last_was_zero = is_zero return last_point def draw_line(cairo_ct, y, h, points): if not len(points): return # pragma: no cover last_point = _line_helper(cairo_ct, y + h, points) if not last_point: # Nothing to draw return # Paint the line cairo_ct.stroke() def draw_fill(cairo_ct, x, y, w, h, points, taper=False): if not len(points): return # pragma: no cover _line_helper(cairo_ct, y + h, points, for_fill=True) baseline_y = h + y + 1 if taper: start_x = w + x else: start_x = points[-1][0] # Box out the area to fill cairo_ct.line_to(start_x + 1, baseline_y) cairo_ct.line_to(x - 1, baseline_y) # Paint the fill cairo_ct.fill() class CellRendererSparkline(Gtk.CellRenderer): __gproperties__ = { # 'name': (GObject.TYPE_*, # nickname, long desc, (type related args), mode) # Type related args can be min, max for int (etc.), or default value # for strings and bool 'data_array': (GObject.TYPE_PYOBJECT, "Data Array", "Array of data points for the graph", GObject.PARAM_READWRITE), 'reversed': (GObject.TYPE_BOOLEAN, "Reverse data", "Process data from back to front.", 0, GObject.PARAM_READWRITE), } def __init__(self): Gtk.CellRenderer.__init__(self) self.data_array = [] self.num_sets = 0 self.filled = True self.reversed = False self.rgb = None def do_render(self, cr, widget, background_area, cell_area, flags): # cr : Cairo context # widget : GtkWidget instance # background_area : GdkRectangle: entire cell area # cell_area : GdkRectangle: area normally rendered by cell # flags : flags that affect rendering # flags = Gtk.CELL_RENDERER_SELECTED, Gtk.CELL_RENDERER_PRELIT, # Gtk.CELL_RENDERER_INSENSITIVE or Gtk.CELL_RENDERER_SORTED ignore = widget ignore = background_area ignore = flags # Indent of the gray border around the graph BORDER_PADDING = 2 # Indent of graph from border GRAPH_INDENT = 2 GRAPH_PAD = (BORDER_PADDING + GRAPH_INDENT) # We don't use yalign, since we expand to the entire height ignore = self.get_property("yalign") xalign = self.get_property("xalign") # Set up graphing bounds graph_x = (cell_area.x + GRAPH_PAD) graph_y = (cell_area.y + GRAPH_PAD) graph_width = (cell_area.width - (GRAPH_PAD * 2)) graph_height = (cell_area.height - (GRAPH_PAD * 2)) pixels_per_point = (graph_width // max(1, len(self.data_array) - 1)) # Graph width needs to be some multiple of the amount of data points # we have graph_width = (pixels_per_point * max(1, len(self.data_array) - 1)) # Recalculate border width based on the amount we are graphing border_width = graph_width + (GRAPH_INDENT * 2) # Align the widget empty_space = cell_area.width - border_width - (BORDER_PADDING * 2) if empty_space: xalign_space = int(empty_space * xalign) cell_area.x += xalign_space graph_x += xalign_space cr.set_line_width(3) # 1 == LINE_CAP_ROUND cr.set_line_cap(1) # Draw gray graph border cr.set_source_rgb(0.8828125, 0.8671875, 0.8671875) cr.rectangle(cell_area.x + BORDER_PADDING, cell_area.y + BORDER_PADDING, border_width, cell_area.height - (BORDER_PADDING * 2)) cr.stroke() # Fill in basecolor box inside graph outline cr.set_source_rgb(BASECOLOR.red, BASECOLOR.green, BASECOLOR.blue) cr.rectangle(cell_area.x + BORDER_PADDING, cell_area.y + BORDER_PADDING, border_width, cell_area.height - (BORDER_PADDING * 2)) cr.fill() def get_y(index): baseline_y = graph_y + graph_height n = index if self.reversed: n = (len(self.data_array) - index - 1) val = self.data_array[n] y = baseline_y - (graph_height * val) y = max(graph_y, y) y = min(graph_y + graph_height, y) return y points = [] for index in range(0, len(self.data_array)): x = int(((index * pixels_per_point) + graph_x)) y = int(get_y(index)) points.append((x, y)) cell_area.x = graph_x cell_area.y = graph_y cell_area.width = graph_width cell_area.height = graph_height # Set color to dark blue for the actual sparkline cr.set_line_width(2) cr.set_source_rgb(0.421875, 0.640625, 0.73046875) draw_line(cr, cell_area.y, cell_area.height, points) # Set color to light blue for the fill cr.set_source_rgba(0.71484375, 0.84765625, 0.89453125, .5) draw_fill(cr, cell_area.x, cell_area.y, cell_area.width, cell_area.height, points) return def do_get_size(self, widget, cell_area=None): ignore = widget ignore = cell_area FIXED_WIDTH = len(self.data_array) FIXED_HEIGHT = 15 xpad = self.get_property("xpad") ypad = self.get_property("ypad") xoffset = 0 yoffset = 0 width = ((xpad * 2) + FIXED_WIDTH) height = ((ypad * 2) + FIXED_HEIGHT) return (xoffset, yoffset, width, height) # Properties are passed to use with "-" in the name, but python # variables can't be named like that def _sanitize_param_spec_name(self, name): return name.replace("-", "_") def do_get_property(self, param_spec): # pragma: no cover name = self._sanitize_param_spec_name(param_spec.name) return getattr(self, name) def do_set_property(self, param_spec, value): name = self._sanitize_param_spec_name(param_spec.name) setattr(self, name, value) def set_property(self, *args, **kwargs): # Make pylint happy return Gtk.CellRenderer.set_property(self, *args, **kwargs) class Sparkline(Gtk.DrawingArea): __gproperties__ = { # 'name': (GObject.TYPE_*, # nickname, long desc, (type related args), mode) # Type related args can be min, max for int (etc.), or default value # for strings and bool 'data_array': (GObject.TYPE_PYOBJECT, "Data Array", "Array of data points for the graph", GObject.PARAM_READWRITE), 'filled': (GObject.TYPE_BOOLEAN, 'Filled', 'the foo of the object', 1, GObject.PARAM_READWRITE), 'num_sets': (GObject.TYPE_INT, "Number of sets", "Number of data sets to graph", 1, 2, 1, GObject.PARAM_READWRITE), 'reversed': (GObject.TYPE_BOOLEAN, "Reverse data", "Process data from back to front.", 0, GObject.PARAM_READWRITE), 'rgb': (GObject.TYPE_PYOBJECT, "rgb array", "List of rgb values", GObject.PARAM_READWRITE), } def __init__(self): Gtk.DrawingArea.__init__(self) self._data_array = [] self.num_sets = 1 self.filled = True self.reversed = False self.rgb = [] ctxt = self.get_style_context() ctxt.add_class(Gtk.STYLE_CLASS_ENTRY) def set_data_array(self, val): self._data_array = val self.queue_draw() def get_data_array(self): return self._data_array data_array = property(get_data_array, set_data_array) def do_draw(self, cr): cr.save() window = self.get_window() w = window.get_width() h = window.get_height() points_per_set = (len(self.data_array) // self.num_sets) pixels_per_point = (float(w) / (float((points_per_set - 1) or 1))) widget = self ctx = widget.get_style_context() # This draws the light gray backing rectangle Gtk.render_background(ctx, cr, 0, 0, w - 1, h - 1) # This draws the marker ticks max_ticks = 4 for index in range(1, max_ticks): Gtk.render_line(ctx, cr, 1, (h // max_ticks) * index, w - 2, (h // max_ticks) * index) # Foreground-color graphics context # This draws the black border Gtk.render_frame(ctx, cr, 0, 0, w - 1, h - 1) # Draw the actual sparkline def get_y(dataset, index): baseline_y = h n = dataset * points_per_set if self.reversed: n += (points_per_set - index - 1) else: n += index val = self.data_array[n] return baseline_y - ((h - 1) * val) cr.set_line_width(2) for dataset in range(0, self.num_sets): if len(self.rgb) == (self.num_sets * 3): cr.set_source_rgb(self.rgb[(dataset * 3)], self.rgb[(dataset * 3) + 1], self.rgb[(dataset * 1) + 2]) points = [] for index in range(0, points_per_set): x = index * pixels_per_point y = get_y(dataset, index) points.append((int(x), int(y))) if self.num_sets == 1: pass draw_line(cr, 0, h, points) if self.filled: # Fixes a fully filled graph from having an oddly # tapered in end (bug 560913). Need to figure out # what's really going on. points = [(0, h)] + points draw_fill(cr, 0, 0, w, h, points, taper=True) cr.restore() return 0 def do_size_request(self, requisition): # pragma: no cover width = len(self.data_array) / self.num_sets height = 20 requisition.width = width requisition.height = height # Properties are passed to use with "-" in the name, but python # variables can't be named like that def _sanitize_param_spec_name(self, name): return name.replace("-", "_") def do_get_property(self, param_spec): # pragma: no cover name = self._sanitize_param_spec_name(param_spec.name) return getattr(self, name) def do_set_property(self, param_spec, value): name = self._sanitize_param_spec_name(param_spec.name) setattr(self, name, value) # These make pylint happy def set_property(self, *args, **kwargs): return Gtk.DrawingArea.set_property(self, *args, **kwargs) def show(self, *args, **kwargs): return Gtk.DrawingArea.show(self, *args, **kwargs) def destroy(self, *args, **kwargs): return Gtk.DrawingArea.destroy(self, *args, **kwargs)