2009-07-22 18:21:33 +04:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301 USA.
#
2013-04-12 01:16:33 +04:00
# pylint: disable=E0611
2012-05-14 17:24:56 +04:00
from gi . repository import GObject
from gi . repository import Gtk
2013-04-12 01:16:33 +04:00
# pylint: enable=E0611
2013-04-12 01:25:28 +04:00
2013-04-12 16:26:21 +04:00
# pylint: disable=E1101
# pylint can't detect functions we inheirit from Gtk, ex:
# Instance of 'Sparkline' has no 'get_style_context' member
2009-07-29 02:01:20 +04:00
def rect_print ( name , rect ) :
2012-11-08 17:15:02 +04:00
# For debugging
2009-07-29 02:01:20 +04:00
print ( " %s : height= %d , width= %d , x= %d , y= %d " %
( name , rect . height , rect . width , rect . x , rect . y ) )
2009-07-22 18:21:33 +04:00
2012-05-14 17:24:56 +04:00
def _line_helper ( cairo_ct , x , y , w , h , points , for_fill = False ) :
2012-11-08 17:15:02 +04:00
ignore = w
2012-05-14 17:24:56 +04:00
bottom_baseline = y + h
2009-07-29 02:01:20 +04:00
last_was_zero = False
last_point = None
for index in range ( 0 , len ( points ) ) :
2009-07-22 18:21:33 +04:00
x , y = points [ index ]
2009-07-29 02:01:20 +04:00
# 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
2012-05-14 17:24:56 +04:00
# the same area as the parent sparkline: fill is one pixel short
# to not overwrite the spark line
2009-07-29 02:01:20 +04:00
if for_fill :
if index == 0 :
x - = 1
elif index == ( len ( points ) - 1 ) :
x + = 1
elif last_was_zero and is_zero :
y + = 1
2009-07-22 18:21:33 +04:00
if index == 0 :
cairo_ct . move_to ( x , y )
2009-07-29 02:01:20 +04:00
elif last_was_zero and is_zero and not for_fill :
cairo_ct . move_to ( x , y )
2009-07-22 18:21:33 +04:00
else :
cairo_ct . line_to ( x , y )
2009-07-29 02:01:20 +04:00
last_point = ( x , y )
2009-07-22 18:21:33 +04:00
2009-07-29 02:01:20 +04:00
last_was_zero = is_zero
return last_point
2012-11-08 17:15:02 +04:00
2012-05-14 17:24:56 +04:00
def draw_line ( cairo_ct , x , y , w , h , points ) :
2009-07-29 02:01:20 +04:00
if not len ( points ) :
return
2012-05-14 17:24:56 +04:00
last_point = _line_helper ( cairo_ct , x , y , w , h , points )
2009-07-29 02:01:20 +04:00
if not last_point :
# Nothing to draw
return
# Paint the line
cairo_ct . stroke ( )
2012-11-08 17:15:02 +04:00
2012-05-14 17:24:56 +04:00
def draw_fill ( cairo_ct , x , y , w , h , points , taper = False ) :
2009-07-29 02:01:20 +04:00
if not len ( points ) :
return
2012-11-08 17:15:02 +04:00
_line_helper ( cairo_ct , x , y , w , h , points , for_fill = True )
2009-07-29 02:01:20 +04:00
2012-05-14 17:24:56 +04:00
baseline_y = h + y + 1
2009-07-29 02:01:20 +04:00
if taper :
2012-05-14 17:24:56 +04:00
start_x = w + x
2009-07-29 02:01:20 +04:00
else :
2012-05-14 17:24:56 +04:00
start_x = points [ - 1 ] [ 0 ]
2009-07-29 02:01:20 +04:00
# Box out the area to fill
2012-05-14 17:24:56 +04:00
cairo_ct . line_to ( start_x + 1 , baseline_y )
cairo_ct . line_to ( x - 1 , baseline_y )
2009-07-29 02:01:20 +04:00
# Paint the fill
cairo_ct . fill ( )
2009-07-22 18:21:33 +04:00
2012-05-14 17:24:56 +04:00
class CellRendererSparkline ( Gtk . CellRenderer ) :
2009-07-22 18:21:33 +04:00
__gproperties__ = {
2012-05-14 17:24:56 +04:00
# 'name' : (GObject.TYPE_*,
2009-07-22 18:21:33 +04:00
# nickname, long desc, (type related args), mode)
# Type related args can be min, max for int (etc.), or default value
# for strings and bool
2012-05-14 17:24:56 +04:00
' data_array ' : ( GObject . TYPE_PYOBJECT , " Data Array " ,
2009-07-22 18:21:33 +04:00
" Array of data points for the graph " ,
2012-05-14 17:24:56 +04:00
GObject . PARAM_READWRITE ) ,
' reversed ' : ( GObject . TYPE_BOOLEAN , " Reverse data " ,
2009-07-22 18:21:33 +04:00
" Process data from back to front. " ,
2012-05-14 17:24:56 +04:00
0 , GObject . PARAM_READWRITE ) ,
2009-07-22 18:21:33 +04:00
}
def __init__ ( self ) :
2012-05-14 17:24:56 +04:00
Gtk . CellRenderer . __init__ ( self )
2009-07-22 18:21:33 +04:00
self . data_array = [ ]
self . num_sets = 0
self . filled = True
self . reversed = False
self . rgb = None
2012-05-14 17:24:56 +04:00
def do_render ( self , cr , widget , background_area , cell_area ,
flags ) :
# cr : Cairo context
# widget : GtkWidget instance
2009-07-22 18:21:33 +04:00
# background_area : GdkRectangle: entire cell area
# cell_area : GdkRectangle: area normally rendered by cell
# flags : flags that affect rendering
2012-05-14 17:24:56 +04:00
# flags = Gtk.CELL_RENDERER_SELECTED, Gtk.CELL_RENDERER_PRELIT,
# Gtk.CELL_RENDERER_INSENSITIVE or Gtk.CELL_RENDERER_SORTED
2010-12-09 19:22:35 +03:00
ignore = widget
ignore = background_area
ignore = flags
2009-07-29 02:01:20 +04:00
# 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 )
2009-07-29 02:15:48 +04:00
# We don't use yalign, since we expand to the entire height
2012-05-14 17:24:56 +04:00
ignore = self . get_property ( " yalign " )
2009-07-29 02:15:48 +04:00
xalign = self . get_property ( " xalign " )
2009-07-29 02:01:20 +04:00
# 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 / ( ( len ( self . data_array ) or 1 ) - 1 ) )
# Graph width needs to be some multiple of the amount of data points
# we have
graph_width = ( pixels_per_point * ( ( len ( self . data_array ) or 1 ) - 1 ) )
# Recalculate border width based on the amount we are graphing
border_width = graph_width + ( GRAPH_INDENT * 2 )
2009-07-29 02:15:48 +04:00
# 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
2012-05-14 17:24:56 +04:00
cr . set_line_width ( 3 )
2013-04-12 01:25:28 +04:00
# 1 == LINE_CAP_ROUND
cr . set_line_cap ( 1 )
2009-07-29 02:01:20 +04:00
# Draw gray graph border
2012-05-14 17:24:56 +04:00
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 ( )
2009-07-29 02:01:20 +04:00
# Fill in white box inside graph outline
2012-05-14 17:24:56 +04:00
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 ( )
2009-07-29 02:01:20 +04:00
2009-07-22 18:21:33 +04:00
def get_y ( index ) :
2009-07-29 02:01:20 +04:00
baseline_y = graph_y + graph_height
2009-07-22 18:21:33 +04:00
if self . reversed :
n = ( len ( self . data_array ) - index - 1 )
else :
n = index
val = self . data_array [ n ]
2009-07-29 02:01:20 +04:00
y = baseline_y - ( graph_height * val )
2009-07-22 18:21:33 +04:00
2009-07-29 02:01:20 +04:00
y = max ( graph_y , y )
y = min ( graph_y + graph_height , y )
return y
2009-07-22 18:21:33 +04:00
points = [ ]
for index in range ( 0 , len ( self . data_array ) ) :
2009-07-29 02:01:20 +04:00
x = int ( ( ( index * pixels_per_point ) + graph_x ) )
y = int ( get_y ( index ) )
2009-07-22 18:21:33 +04:00
2009-07-29 02:01:20 +04:00
points . append ( ( x , y ) )
2009-07-22 18:21:33 +04:00
2009-07-29 02:01:20 +04:00
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
2012-05-14 17:24:56 +04:00
cr . set_line_width ( 2 )
cr . set_source_rgb ( 0.421875 , 0.640625 , 0.73046875 )
draw_line ( cr ,
cell_area . x , cell_area . y ,
cell_area . width , cell_area . height ,
points )
2009-07-29 02:01:20 +04:00
# Set color to light blue for the fill
2012-05-14 17:24:56 +04:00
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 )
2009-07-22 18:21:33 +04:00
return
def do_get_size ( self , widget , cell_area = None ) :
2010-12-09 19:22:35 +03:00
ignore = widget
2009-07-29 02:01:20 +04:00
FIXED_WIDTH = len ( self . data_array )
FIXED_HEIGHT = 15
xpad = self . get_property ( " xpad " )
ypad = self . get_property ( " ypad " )
if cell_area :
2012-05-14 17:24:56 +04:00
# What to do here? haven't encountered this in practice
2009-07-29 02:01:20 +04:00
xoffset = 0
yoffset = 0
else :
xoffset = 0
yoffset = 0
width = ( ( xpad * 2 ) + FIXED_WIDTH )
height = ( ( ypad * 2 ) + FIXED_HEIGHT )
2009-07-22 18:21:33 +04:00
return ( xoffset , yoffset , width , height )
2012-05-14 17:24:56 +04:00
# Properties are passed to use with "-" in the name, but python
# variables can't be named like that
2009-07-22 18:21:33 +04:00
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 )
2013-04-12 16:26:21 +04:00
def set_property ( self , * args , * * kwargs ) :
# Make pylint happy
return Gtk . CellRenderer . set_property ( self , * args , * * kwargs )
2012-05-14 17:24:56 +04:00
class Sparkline ( Gtk . DrawingArea ) :
2009-07-22 18:21:33 +04:00
__gproperties__ = {
2012-05-14 17:24:56 +04:00
# 'name' : (GObject.TYPE_*,
2009-07-22 18:21:33 +04:00
# nickname, long desc, (type related args), mode)
# Type related args can be min, max for int (etc.), or default value
# for strings and bool
2012-05-14 17:24:56 +04:00
' data_array ' : ( GObject . TYPE_PYOBJECT , " Data Array " ,
2009-07-22 18:21:33 +04:00
" Array of data points for the graph " ,
2012-05-14 17:24:56 +04:00
GObject . PARAM_READWRITE ) ,
' filled ' : ( GObject . TYPE_BOOLEAN , ' Filled ' , ' the foo of the object ' ,
2009-07-22 18:21:33 +04:00
1 ,
2012-05-14 17:24:56 +04:00
GObject . PARAM_READWRITE ) ,
' num_sets ' : ( GObject . TYPE_INT , " Number of sets " ,
2009-07-22 18:21:33 +04:00
" Number of data sets to graph " ,
2012-05-14 17:24:56 +04:00
1 , 2 , 1 , GObject . PARAM_READWRITE ) ,
' reversed ' : ( GObject . TYPE_BOOLEAN , " Reverse data " ,
2009-07-22 18:21:33 +04:00
" Process data from back to front. " ,
2012-05-14 17:24:56 +04:00
0 , GObject . PARAM_READWRITE ) ,
' rgb ' : ( GObject . TYPE_PYOBJECT , " rgb array " , " List of rgb values " ,
GObject . PARAM_READWRITE ) ,
2009-07-22 18:21:33 +04:00
}
def __init__ ( self ) :
2012-05-14 17:24:56 +04:00
Gtk . DrawingArea . __init__ ( self )
2009-07-22 18:21:33 +04:00
self . _data_array = [ ]
self . num_sets = 1
self . filled = True
self . reversed = False
self . rgb = [ ]
2012-05-14 17:24:56 +04:00
ctxt = self . get_style_context ( )
ctxt . add_class ( Gtk . STYLE_CLASS_ENTRY )
2009-07-22 18:21:33 +04:00
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 )
2012-05-14 17:24:56 +04:00
def do_draw ( self , cr ) :
cr . save ( )
2009-07-22 18:21:33 +04:00
2012-05-14 17:24:56 +04:00
window = self . get_window ( )
w = window . get_width ( )
h = window . get_height ( )
2009-07-22 18:21:33 +04:00
points_per_set = ( len ( self . data_array ) / self . num_sets )
2012-05-14 17:24:56 +04:00
pixels_per_point = ( float ( w ) /
2009-07-22 21:21:27 +04:00
( float ( ( points_per_set - 1 ) or 1 ) ) )
2009-07-22 18:21:33 +04:00
2012-05-14 17:24:56 +04:00
widget = self
ctx = widget . get_style_context ( )
2009-07-22 18:21:33 +04:00
# This draws the light gray backing rectangle
2012-05-14 17:24:56 +04:00
Gtk . render_background ( ctx , cr , 0 , 0 , w - 1 , h - 1 )
2009-07-22 18:21:33 +04:00
# This draws the marker ticks
max_ticks = 4
2012-05-14 17:24:56 +04:00
for index in range ( 1 , max_ticks ) :
Gtk . render_line ( ctx , cr , 1 ,
( h / max_ticks ) * index ,
w - 2 ,
( h / max_ticks ) * index )
2009-07-22 18:21:33 +04:00
2010-02-10 20:46:12 +03:00
# Foreground-color graphics context
# This draws the black border
2012-05-14 17:24:56 +04:00
Gtk . render_frame ( ctx , cr , 0 , 0 , w - 1 , h - 1 )
2010-02-10 20:46:12 +03:00
2009-07-22 18:21:33 +04:00
# Draw the actual sparkline
def get_y ( dataset , index ) :
2012-05-14 17:24:56 +04:00
baseline_y = h
2009-07-22 18:21:33 +04:00
n = dataset * points_per_set
if self . reversed :
n + = ( points_per_set - index - 1 )
else :
n + = index
val = self . data_array [ n ]
2012-05-14 17:24:56 +04:00
return baseline_y - ( ( h - 1 ) * val )
2009-07-22 18:21:33 +04:00
2012-05-14 17:24:56 +04:00
cr . set_line_width ( 2 )
2009-07-22 18:21:33 +04:00
for dataset in range ( 0 , self . num_sets ) :
if len ( self . rgb ) == ( self . num_sets * 3 ) :
2012-05-14 17:24:56 +04:00
cr . set_source_rgb ( self . rgb [ ( dataset * 3 ) ] ,
2009-07-22 18:21:33 +04:00
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
2009-07-29 02:01:20 +04:00
2012-05-14 17:24:56 +04:00
draw_line ( cr , 0 , 0 , w , h , points )
2009-07-29 02:01:20 +04:00
if self . filled :
2013-04-17 17:09:53 +04:00
# Fixes a fully filled graph from having an oddly
# tapered in end (bug 560913). Need to figure out
# what's really going on.
2012-05-14 17:24:56 +04:00
points = [ ( 0 , h ) ] + points
draw_fill ( cr , 0 , 0 , w , h , points , taper = True )
cr . restore ( )
2009-07-22 18:21:33 +04:00
return 0
def do_size_request ( self , requisition ) :
width = len ( self . data_array ) / self . num_sets
height = 20
requisition . width = width
requisition . height = height
2012-05-14 17:24:56 +04:00
# Properties are passed to use with "-" in the name, but python
# variables can't be named like that
2009-07-22 18:21:33 +04:00
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 )
2013-04-12 16:26:21 +04:00
# 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 )