390 lines
12 KiB
Python
390 lines
12 KiB
Python
# 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
|
|
|
|
|
|
def rect_print(name, rect):
|
|
# 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
|
|
|
|
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
|
|
|
|
_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 white box inside graph outline
|
|
cr.set_source_rgb(1, 1, 1)
|
|
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
|
|
|
|
if self.reversed:
|
|
n = (len(self.data_array) - index - 1)
|
|
else:
|
|
n = index
|
|
|
|
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
|
|
|
|
FIXED_WIDTH = len(self.data_array)
|
|
FIXED_HEIGHT = 15
|
|
xpad = self.get_property("xpad")
|
|
ypad = self.get_property("ypad")
|
|
|
|
if cell_area:
|
|
# What to do here? haven't encountered this in practice
|
|
xoffset = 0
|
|
yoffset = 0
|
|
else:
|
|
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):
|
|
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):
|
|
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):
|
|
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)
|