[billreminder] Updated graphing lib to latest hamster-project code.
- From: Og B. Maciel <ogmaciel src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [billreminder] Updated graphing lib to latest hamster-project code.
- Date: Wed, 30 Dec 2009 23:19:10 +0000 (UTC)
commit 8bdfbe29b262b87f6b041555062a29a54e0760dd
Author: Og B. Maciel <ogmaciel gnome org>
Date: Wed Dec 30 10:56:49 2009 -0500
Updated graphing lib to latest hamster-project code.
src/gui/widgets/charting.py | 252 ++++++++++++++++++++++++++----------------
src/gui/widgets/graphics.py | 110 +++++++++++++------
2 files changed, 234 insertions(+), 128 deletions(-)
---
diff --git a/src/gui/widgets/charting.py b/src/gui/widgets/charting.py
index 75a9616..cc3ac36 100644
--- a/src/gui/widgets/charting.py
+++ b/src/gui/widgets/charting.py
@@ -139,6 +139,7 @@ class Chart(graphics.Area):
# other stuff
self.bars = []
self.keys = []
+ self.data = None
self.stack_keys = []
self.key_colors = {} # key:color dictionary. if key's missing will grab basecolor
@@ -176,7 +177,7 @@ class Chart(graphics.Area):
self.show()
- if not data: #if there is no data, let's just draw blank
+ if not data: #if there is no data, just draw blank
self.redraw_canvas()
return
@@ -244,6 +245,9 @@ class BarChart(Chart):
return
context = self.context
+ context.set_line_width(1)
+
+
# determine graph dimensions
if self.show_stack_labels:
legend_width = self.legend_width or self.longest_label(self.keys)
@@ -276,10 +280,6 @@ class BarChart(Chart):
self.context.stroke()
- bar_width = min(self.graph_width / float(len(self.keys)),
- self.max_bar_width)
- gap = bar_width * 0.05
-
# bars and keys
max_bar_size = self.graph_height
#make sure bars don't hit the ceiling
@@ -290,12 +290,23 @@ class BarChart(Chart):
prev_label_end = None
self.layout.set_width(-1)
- for i in range(len(self.keys)):
+ exes = {}
+ x = 0
+ bar_width = min(self.graph_width / float(len(self.keys)), self.max_bar_width)
+ for i, key in enumerate(self.keys):
+ exes[key] = (x + self.graph_x, round(bar_width - 1))
+
+ x = x + round(bar_width)
+ bar_width = min(self.max_bar_width,
+ (self.graph_width - x) / float(max(1, len(self.keys) - i - 1)))
+
+
+ for key, bar, data in zip(self.keys, self.bars, self.data):
self.set_color(graphics.Colors.aluminium[5]);
- self.layout.set_text(self.keys[i])
+ self.layout.set_text(key)
label_w, label_h = self.layout.get_pixel_size()
- intended_x = (bar_width * i) + (bar_width - label_w) / 2.0 + self.graph_x
+ intended_x = exes[key][0] + (exes[key][1] - label_w) / 2
if not prev_label_end or intended_x > prev_label_end:
self.context.move_to(intended_x, self.graph_height + 4)
@@ -306,39 +317,41 @@ class BarChart(Chart):
bar_start = 0
base_color = self.bar_base_color or (220, 220, 220)
- bar_x = round(self.graph_x + bar_width * i + gap)
-
+
if self.stack_keys:
- for j, bar in enumerate(self.bars[i]):
- if bar.size > 0:
- bar_size = round(max_bar_size * bar.size)
+ remaining_fractions, remaining_pixels = 1, max_bar_size
+
+ for j, stack_bar in enumerate(bar):
+ if stack_bar.size > 0:
+ bar_size = round(remaining_pixels * (stack_bar.size / remaining_fractions))
+ remaining_fractions -= stack_bar.size
+ remaining_pixels -= bar_size
+
bar_start += bar_size
- last_color = self.stack_key_colors.get(self.stack_keys[j],
- self.get_bar_color(j))
- self.draw_bar(bar_x,
+ last_color = self.stack_key_colors.get(self.stack_keys[j]) or self.get_bar_color(j)
+ self.draw_bar(exes[key][0],
self.graph_height - bar_start,
- round(bar_width - (gap * 2)),
+ exes[key][1],
bar_size,
last_color)
else:
- bar_size = round(max_bar_size * self.bars[i].size)
+ bar_size = round(max_bar_size * bar.size)
bar_start = bar_size
- last_color = self.key_colors.get(self.keys[i],
- base_color)
- self.draw_bar(bar_x,
+ last_color = self.key_colors.get(key) or base_color
+ self.draw_bar(exes[key][0],
self.graph_y + self.graph_height - bar_size,
- round(bar_width - (gap * 2)),
+ exes[key][1],
bar_size,
last_color)
- if self.values_on_bars: # it's either stack labels or values at the end for now
+ if self.values_on_bars: # it is either stack labels or values at the end for now
if self.stack_keys:
- total_value = sum(self.data[i])
+ total_value = sum(data[i])
else:
- total_value = self.data[i]
+ total_value = data[i]
self.layout.set_width(-1)
self.layout.set_text(self.value_format % total_value)
@@ -350,7 +363,8 @@ class BarChart(Chart):
else:
label_y = self.graph_y + self.graph_height - bar_start - label_h + 5
- context.move_to(self.graph_x + (bar_width * i) + (bar_width - label_w) / 2.0, label_y)
+ context.move_to(self.exes[key][0] + (self.exes[key][1] - label_w) / 2.0,
+ label_y)
# we are in the bar so make sure that the font color is distinguishable
if colorsys.rgb_to_hls(*graphics.Colors.rgb(last_color))[1] < 150:
@@ -359,17 +373,7 @@ class BarChart(Chart):
self.set_color(graphics.Colors.aluminium[5])
context.show_layout(self.layout)
-
- # values on bars
- if self.stack_keys:
- total_value = sum(self.data[i])
- else:
- total_value = self.data[i]
-
- #fill with white background (necessary for those dragging cases)
- if self.background:
- self.fill_area(0, 0, legend_width, self.height, self.background)
#white grid and scale values
self.layout.set_width(-1)
@@ -382,7 +386,7 @@ class BarChart(Chart):
context.set_line_width(1)
for i in range(grid_stride, int(self.max_value), grid_stride):
- y = max_bar_size * (i / self.max_value)
+ y = round(max_bar_size * (i / self.max_value)) + 0.5
if self.show_scale:
self.layout.set_text(self.value_format % i)
@@ -392,17 +396,13 @@ class BarChart(Chart):
self.set_color(graphics.Colors.aluminium[4])
context.show_layout(self.layout)
- self.set_color((255, 255, 255))
+ self.set_color("#ffffff")
self.context.move_to(legend_width, y)
self.context.line_to(self.width, y)
#stack keys
- context.save()
if self.show_stack_labels:
- context.set_line_width(1)
- context.set_antialias(cairo.ANTIALIAS_DEFAULT)
-
#put series keys
self.set_color(graphics.Colors.aluminium[5]);
@@ -411,9 +411,9 @@ class BarChart(Chart):
# if labels are at end, then we need show them for the last bar!
if self.labels_at_end:
- factors = self.bars[0]
- else:
factors = self.bars[-1]
+ else:
+ factors = self.bars[0]
if isinstance(factors, Bar):
factors = [factors]
@@ -462,7 +462,6 @@ class BarChart(Chart):
context.line_to(line_x2, round(y + bar_size / 2))
context.stroke()
- context.restore()
class HorizontalBarChart(Chart):
@@ -489,17 +488,21 @@ class HorizontalBarChart(Chart):
self.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
- if not self.data: #if we have nothing, let's go home
+ if not self.data: # go home if we have nothing
return
-
- bar_width = int(self.graph_height / float(rowcount))
- bar_width = min(bar_width, self.max_bar_width)
+ positions = {}
+ y = 0
+ bar_width = min(self.graph_height / float(len(self.keys)), self.max_bar_width)
+ for i, key in enumerate(self.keys):
+ positions[key] = (y + self.graph_y, round(bar_width - 1))
+
+ y = y + round(bar_width)
+ bar_width = min(self.max_bar_width,
+ (self.graph_height - y) / float(max(1, len(self.keys) - i - 1)))
-
- max_bar_size = self.graph_width - 15
- gap = bar_width * 0.05
+ max_bar_size = self.graph_width - 15
self.layout.set_alignment(pango.ALIGN_RIGHT)
self.layout.set_ellipsize(pango.ELLIPSIZE_END)
@@ -519,44 +522,41 @@ class HorizontalBarChart(Chart):
self.layout.set_text(label)
label_w, label_h = self.layout.get_pixel_size()
- context.move_to(0, (bar_width * i) + (bar_width - label_h) / 2)
+ context.move_to(0, positions[label][0] + (positions[label][1] - label_h) / 2)
context.show_layout(self.layout)
base_color = self.bar_base_color or (220, 220, 220)
- gap = bar_width * 0.05
-
- bar_y = round(self.graph_y + (bar_width * i) + gap)
-
last_color = (255,255,255)
-
if self.stack_keys:
bar_start = 0
- for j, bar in enumerate(self.bars[i]):
- if bar.size > 0:
- bar_size = round(max_bar_size * bar.size)
- bar_height = round(bar_width - (gap * 2))
-
- last_color = self.stack_key_colors.get(self.stack_keys[j],
- self.get_bar_color(j))
+ remaining_fractions, remaining_pixels = 1, max_bar_size
+
+ for j, stack_bar in enumerate(self.bars[i]):
+ if stack_bar.size > 0:
+ bar_size = round(remaining_pixels * (stack_bar.size / remaining_fractions))
+ remaining_fractions -= stack_bar.size
+ remaining_pixels -= bar_size
+
+ last_color = self.stack_key_colors.get(self.stack_keys[j]) or self.get_bar_color(j)
self.draw_bar(self.graph_x + bar_start,
- bar_y,
+ positions[label][0],
bar_size,
- bar_height,
+ positions[label][1],
last_color)
bar_start += bar_size
else:
bar_size = round(max_bar_size * self.bars[i].size)
bar_start = bar_size
- bar_height = round(bar_width - (gap * 2))
+ last_color = self.key_colors.get(self.keys[i]) or base_color
- last_color = self.key_colors.get(self.keys[i],
- base_color)
-
- self.draw_bar(self.graph_x, bar_y, bar_size, bar_height,
- last_color)
+ self.draw_bar(self.graph_x,
+ positions[label][0],
+ bar_size,
+ positions[label][1],
+ last_color)
# values on bars
if self.stack_keys:
@@ -568,7 +568,7 @@ class HorizontalBarChart(Chart):
self.layout.set_text(self.value_format % total_value)
label_w, label_h = self.layout.get_pixel_size()
- vertical_padding = (bar_width - (bar_width + label_h) / 2.0 ) / 2.0
+ vertical_padding = max((positions[label][1] - label_h) / 2.0, 1)
if bar_start - vertical_padding < label_w:
label_x = self.graph_x + bar_start + vertical_padding
self.set_color(graphics.Colors.aluminium[5])
@@ -581,7 +581,7 @@ class HorizontalBarChart(Chart):
label_x = self.graph_x + bar_start - label_w - vertical_padding
- context.move_to(label_x, self.graph_y + (bar_width * i) + (bar_width - label_h) / 2.0)
+ context.move_to(label_x, positions[label][0] + (positions[label][1] - label_h) / 2.0)
context.show_layout(self.layout)
context.stroke()
@@ -592,6 +592,10 @@ class HorizontalBarChart(Chart):
class HorizontalDayChart(Chart):
"""Pretty much a horizontal bar chart, except for values it expects tuple
of start and end time, and the whole thing hangs in air"""
+ def __init__(self, *args, **kwargs):
+ Chart.__init__(self, *args, **kwargs)
+ self.start_time, self.end_time = None, None
+
def plot_day(self, keys, data, start_time = None, end_time = None):
self.keys, self.data = keys, data
self.start_time, self.end_time = start_time, end_time
@@ -600,6 +604,7 @@ class HorizontalDayChart(Chart):
def on_expose(self):
context = self.context
+
Chart.on_expose(self)
rowcount, keys = len(self.keys), self.keys
@@ -633,43 +638,41 @@ class HorizontalDayChart(Chart):
return
- bar_width = int(self.graph_height / float(rowcount))
- bar_width = min(bar_width, self.max_bar_width)
+ self.context.translate(0.5, 0.5)
+
+ positions = {}
+ y = 0
+ bar_width = min(self.graph_height / float(len(self.keys)), self.max_bar_width)
+ for i, key in enumerate(self.keys):
+ positions[key] = (y + self.graph_y, round(bar_width - 1))
+
+ y = y + round(bar_width)
+ bar_width = min(self.max_bar_width,
+ (self.graph_height - y) / float(max(1, len(self.keys) - i - 1)))
+
max_bar_size = self.graph_width - 15
- gap = bar_width * 0.05
-
self.layout.set_alignment(pango.ALIGN_RIGHT)
self.layout.set_ellipsize(pango.ELLIPSIZE_END)
- context.set_line_width(0)
-
# bars and labels
self.layout.set_width(legend_width * pango.SCALE)
factor = max_bar_size / float(end_hour - start_hour)
-
for i, label in enumerate(keys):
self.set_color(graphics.Colors.aluminium[5])
self.layout.set_text(label)
label_w, label_h = self.layout.get_pixel_size()
- context.move_to(0, (bar_width * i) + (bar_width - label_h) / 2)
+ context.move_to(0, positions[label][0] + (positions[label][1] - label_h) / 2)
context.show_layout(self.layout)
base_color = self.bar_base_color or [220, 220, 220]
- gap = bar_width * 0.05
-
- bar_y = round(self.graph_y + (bar_width * i) + gap)
-
-
- bar_height = round(bar_width - (gap * 2))
-
if isinstance(self.data[i], list) == False:
self.data[i] = [self.data[i]]
@@ -677,10 +680,10 @@ class HorizontalDayChart(Chart):
bar_x = round((row[0]- start_hour) * factor)
bar_size = round((row[1] - start_hour) * factor - bar_x)
- self.draw_bar(self.graph_x + bar_x,
- bar_y,
+ self.draw_bar(self.graph_x + bar_x + 0.5,
+ positions[label][0] + 0.5,
bar_size,
- bar_height,
+ positions[label][1],
base_color)
#white grid and scale values
@@ -689,8 +692,9 @@ class HorizontalDayChart(Chart):
context.set_line_width(1)
pace = ((end_hour - start_hour) / 3) / 60 * 60
+ last_position = positions[keys[-1]]
for i in range(start_hour + 60, end_hour, pace):
- x = (i - start_hour) * factor
+ x = round((i - start_hour) * factor)
minutes = i % (24 * 60)
@@ -698,14 +702,70 @@ class HorizontalDayChart(Chart):
label_w, label_h = self.layout.get_pixel_size()
context.move_to(self.graph_x + x - label_w / 2,
- bar_y + bar_height + 4)
+ last_position[0] + last_position[1] + 4)
self.set_color(graphics.Colors.aluminium[4])
context.show_layout(self.layout)
self.set_color((255, 255, 255))
self.context.move_to(self.graph_x + x, self.graph_y)
- self.context.line_to(self.graph_x + x, bar_y + bar_height)
+ self.context.line_to(self.graph_x + x,
+ last_position[0] + last_position[1])
context.stroke()
+
+
+""" sample usage """
+class BasicWindow:
+ def __init__(self):
+ window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ window.set_title("Hamster Charting")
+ window.set_size_request(400, 300)
+ window.connect("delete_event", lambda *args: gtk.main_quit())
+
+ self.stacked = BarChart(background = "#fafafa",
+ bar_base_color = (220, 220, 220),
+ legend_width = 20,
+ show_stack_labels = True)
+
+ box = gtk.VBox()
+ box.pack_start(self.stacked)
+
+
+ self.series = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]
+ self.stacks = ["x", "y", "z", "a", "b", "c", "d"]
+ self.stack_colors = dict([(stack, None) for stack in self.stacks])
+
+ import random
+ self.data = [[random.randint(0, 10) for j in range(len(self.stacks))] for i in range(len(self.series))]
+
+ color_buttons = gtk.HBox()
+ color_buttons.set_spacing(4)
+
+ for stack in self.stacks:
+ button = gtk.ColorButton()
+ button.connect("color-set", self.on_color_set, stack)
+ color_buttons.pack_start(button)
+
+ box.pack_start(color_buttons, False)
+
+ window.add(box)
+ window.show_all()
+
+ self.plot()
+
+
+ def plot(self):
+ self.stacked.stack_key_colors = self.stack_colors
+ self.stacked.plot(self.series, self.data, self.stacks)
+
+
+ def on_color_set(self, button, stack_idx):
+ self.stack_colors[stack_idx] = button.get_color().to_string()
+ self.plot()
+
+
+if __name__ == "__main__":
+ example = BasicWindow()
+ gtk.main()
diff --git a/src/gui/widgets/graphics.py b/src/gui/widgets/graphics.py
index 3c5f9d7..6897e04 100644
--- a/src/gui/widgets/graphics.py
+++ b/src/gui/widgets/graphics.py
@@ -1,3 +1,22 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster 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 Project Hamster. If not, see <http://www.gnu.org/licenses/>.
+import math
import time, datetime as dt
import gtk, gobject
@@ -93,31 +112,60 @@ class Area(gtk.DrawingArea):
def animate(self, object, params = {}, duration = None, easing = None, callback = None):
- if duration: params["tweenTime"] = duration # if none will fallback to tweener's default
- if easing: params["tweenType"] = easing # if none will fallback to tweener's default
+ if duration: params["tweenTime"] = duration # if none will fallback to tweener default
+ if easing: params["tweenType"] = easing # if none will fallback to tweener default
if callback: params["onCompleteFunction"] = callback
self.tweener.addTween(object, **params)
self.redraw_canvas()
""" drawing on canvas bits """
- def __rectangle(self, x, y, w, h, color, opacity = 0):
- self.set_color(color, opacity)
+ def draw_rect(self, x, y, w, h, corner_radius = 0):
+ if corner_radius <=0:
+ self.context.rectangle(x, y, w, h)
+ return
+
+ # make sure that w + h are larger than 2 * corner_radius
+ corner_radius = min(corner_radius, min(w, h) / 2)
+
+ x2, y2 = x + w, y + h
+
+ half_corner = corner_radius / 2
+
+ self.context.move_to(x + corner_radius, y);
+ self.context.line_to(x2 - corner_radius, y);
+ # top-right
+ self.context.curve_to(x2 - half_corner, y,
+ x2, y + half_corner,
+ x2, y + corner_radius)
+
+ self.context.line_to(x2, y2 - corner_radius);
+ # bottom-right
+ self.context.curve_to(x2, y2 - half_corner,
+ x2 - half_corner, y+h,
+ x2 - corner_radius,y+h)
+
+ self.context.line_to(x + corner_radius, y2);
+ # bottom-left
+ self.context.curve_to(x + half_corner, y2,
+ x, y2 - half_corner,
+ x,y2 - corner_radius)
+
+ self.context.line_to(x, y + corner_radius);
+ # top-left
+ self.context.curve_to(x, y + half_corner,
+ x + half_corner, y,
+ x + corner_radius,y)
+
+
+ def rectangle(self, x, y, w, h, color = None, opacity = 0):
+ if color:
+ self.set_color(color, opacity)
self.context.rectangle(x, y, w, h)
def fill_area(self, x, y, w, h, color, opacity = 0):
- self.context.save()
- self.__rectangle(x, y, w, h, color, opacity)
+ self.rectangle(x, y, w, h, color, opacity)
self.context.fill()
- self.context.restore()
-
- def fill_rectangle(self, x, y, w, h, color, opacity = 0):
- self.context.save()
- self.__rectangle(x, y, w, h, color, opacity)
- self.context.fill_preserve()
- self.set_color(color)
- self.context.stroke()
- self.context.restore()
def set_text(self, text):
# sets text and returns width and height of the layout
@@ -216,7 +264,7 @@ class SampleArea(Area):
def __init__(self):
Area.__init__(self)
self.rect_x, self.rect_y = 100, -100
- self.rect_width, self.rect_height = 50, 50
+ self.rect_width, self.rect_height = 90, 90
self.text_y = -100
@@ -229,30 +277,28 @@ class SampleArea(Area):
self.font_size = 32
self.layout.set_text("Hello, World!")
- self.fill_area(self.rect_x,
- self.rect_y,
+ self.draw_rect(round(self.rect_x),
+ round(self.rect_y),
self.rect_width,
- self.rect_height, (168, 186, 136))
+ self.rect_height,
+ 10)
+
+ self.set_color("#ff00ff")
+ self.context.fill()
self.context.move_to((self.width - self.layout.get_pixel_size()[0]) / 2,
self.text_y)
+ self.set_color("#333")
self.context.show_layout(self.layout)
class BasicWindow:
- # close the window and quit
- def delete_event(self, widget, event, data=None):
- gtk.main_quit()
- return False
-
def __init__(self):
- # Create a new window
- self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
-
- self.window.set_title("Graphics Module")
- self.window.set_size_request(300, 300)
- self.window.connect("delete_event", self.delete_event)
+ window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ window.set_title("Graphics Module")
+ window.set_size_request(300, 300)
+ window.connect("delete_event", lambda *args: gtk.main_quit())
self.graphic = SampleArea()
@@ -264,8 +310,8 @@ class BasicWindow:
box.add_with_properties(button, "expand", False)
- self.window.add(box)
- self.window.show_all()
+ window.add(box)
+ window.show_all()
# drop the hello on init
self.graphic.animate(self.graphic,
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]