[hamster-applet] simplified bar charts
- From: Toms Baugis <tbaugis src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [hamster-applet] simplified bar charts
- Date: Thu, 8 Apr 2010 12:49:19 +0000 (UTC)
commit 48bf446975d5f8de2d607f75ff1f7857e08875d6
Author: Toms Bauģis <toms baugis gmail com>
Date: Thu Apr 8 13:48:58 2010 +0100
simplified bar charts
src/hamster/charting.py | 842 +++++++---------------------------------
src/hamster/overview_totals.py | 58 ++--
src/hamster/stats.py | 28 +-
3 files changed, 188 insertions(+), 740 deletions(-)
---
diff --git a/src/hamster/charting.py b/src/hamster/charting.py
index 446dc00..ca2304d 100644
--- a/src/hamster/charting.py
+++ b/src/hamster/charting.py
@@ -1,6 +1,6 @@
# - coding: utf-8 -
-# Copyright (C) 2008 Toms Bauģis <toms.baugis at gmail.com>
+# Copyright (C) 2008-2010 Toms Bauģis <toms.baugis at gmail.com>
# This file is part of Project Hamster.
@@ -17,695 +17,206 @@
# 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 gtk, gobject
+import pango
+import datetime as dt
+import time
+import graphics
-"""Small charting library that enables you to draw bar and
-horizontal bar charts. This library is not intended for scientific graphs.
-More like some visual clues to the user.
+class Bar(graphics.Shape):
+ def __init__(self, key, value, normalized, label_color):
+ graphics.Shape.__init__(self)
+ self.key, self.value, self.normalized = key, value, normalized
-The whole thing is a bit of minefield, but it can bring pretty decent results
-if you don't ask for much.
+ self.height = 0
+ self.width = 20
+ self.vertical = True
+ self.interactive = True
-For graph options see the Chart class and Chart.plot function
+ self.label = graphics.Label(value, size=8, color=label_color)
+ self.label_background = graphics.Rectangle(self.label.width + 4, self.label.height + 4, 4, visible=False)
+ self.add_child(self.label_background)
+ self.add_child(self.label)
-Author: toms baugis gmail com
-Feel free to contribute - more info at Project Hamster web page:
-http://projecthamster.wordpress.com/
+ def draw_shape(self):
+ # invisible rectangle for the mouse, covering whole area
+ self.graphics.set_color("#000", 0)
+ self.graphics.rectangle(0, 0, self.width, self.height)
+ self.graphics.stroke()
-"""
+ size = round(self.width * self.normalized)
-import gtk, gobject
-import cairo, pango
-import copy
-import math
-from sys import maxint
-import datetime as dt
-import time
-import colorsys
-import logging
+ self.graphics.rectangle(0, 0, size, self.height, 3)
+ self.graphics.rectangle(0, 0, min(size, 3), self.height)
-import graphics
+ self.label.y = (self.height - self.label.height) / 2
+ horiz_offset = min(10, self.label.y * 2)
-def size_list(set, target_set):
- """turns set lenghts into target set - trim it, stretches it, but
- keeps values for cases when lengths match
- """
- set = set[:min(len(set), len(target_set))] #shrink to target
- set += target_set[len(set):] #grow to target
-
- #nest
- for i in range(len(set)):
- if isinstance(set[i], list):
- set[i] = size_list(set[i], target_set[i])
- return set
-
-def get_limits(set, stack_subfactors = True):
- # stack_subfactors indicates whether we should sum up nested lists
- max_value, min_value = -maxint, maxint
- for col in set:
- if type(col) in [int, float]:
- max_value = max(col, max_value)
- min_value = min(col, min_value)
- elif stack_subfactors:
- max_value = max(sum(col), max_value)
- min_value = min(sum(col), min_value)
+ if self.label.width < size - horiz_offset * 2:
+ #if it fits in the bar
+ self.label.x = size - self.label.width - horiz_offset
else:
- for row in col:
- max_value = max(row, max_value)
- min_value = max(row, min_value)
-
- return min_value, max_value
+ self.label.x = size + 3
-
-class Bar(object):
- def __init__(self, value, size = 0):
- self.value = value
- self.size = size
-
- def __repr__(self):
- return str((self.value, self.size))
+ self.label_background.x = self.label.x - 2
+ self.label_background.y = self.label.y - 2
class Chart(graphics.Scene):
- """Chart constructor. Optional arguments:
- self.max_bar_width = pixels. Maximal width of bar. If not specified,
- bars will stretch to fill whole area
- self.legend_width = pixels. Legend width will keep you graph
- from floating around.
- self.animate = Should transitions be animated.
- Defaults to TRUE
- self.framerate = Frame rate for animation. Defaults to 60
-
- self.background = Tripplet-tuple of background color in RGB
- self.chart_background = Tripplet-tuple of chart background color in RGB
- self.bar_base_color = Tripplet-tuple of bar color in RGB
-
- self.show_scale = Should we show scale values. See grid_stride!
- self.grid_stride = Step of grid. If expressed in normalized range
- (0..1), will be treated as percentage.
- Otherwise will be striding through maximal value.
- Defaults to 0. Which is "don't draw"
-
- self.values_on_bars = Should values for each bar displayed on top of
- it.
- self.value_format = Format string for values. Defaults to "%s"
-
- self.show_stack_labels = If the labels of stack bar chart should be
- displayed. Defaults to False
- self.labels_at_end = If stack bars are displayed, this allows to
- show them at right end of graph.
- """
__gsignals__ = {
"bar-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
}
- def __init__(self, **args):
- graphics.Scene.__init__(self)
-
- # options
- self.max_bar_width = args.get("max_bar_width", 500)
- self.legend_width = args.get("legend_width", 0)
- self.animation = args.get("animate", True)
- self.background = args.get("background", None)
- self.chart_background = args.get("chart_background", None)
- self.bar_base_color = args.get("bar_base_color", None)
-
- self.grid_stride = args.get("grid_stride", None)
- self.values_on_bars = args.get("values_on_bars", False)
- self.value_format = args.get("value_format", "%s")
- self.show_scale = args.get("show_scale", False)
-
- self.show_stack_labels = args.get("show_stack_labels", False)
- self.labels_at_end = args.get("labels_at_end", False)
+ def __init__(self, max_bar_width = 20, legend_width = 70, value_format = "%.2f", interactive = True):
+ graphics.Scene.__init__(self)
- self.interactive = args.get("interactive", False) # if the bars are clickable
+ self.selected_keys = [] # keys of selected bars
- # other stuff
self.bars = []
- self.keys = []
- self.data = None
- self.stack_keys = []
+ self.labels = []
- self.key_colors = {} # key:color dictionary. if key's missing will grab basecolor
- self.stack_key_colors = {} # key:color dictionary. if key's missing will grab basecolor
+ self.max_width = max_bar_width
+ self.legend_width = legend_width
+ self.value_format = value_format
+ self.graph_interactive = interactive
+ self.vertical = True
+ self.plot_area = graphics.Sprite(interactive = False)
+ self.add_child(self.plot_area)
- # use these to mark area where the "real" drawing is going on
- self.graph_x, self.graph_y = 0, 0
- self.graph_width, self.graph_height = None, None
-
- self.mouse_bar = None
- if self.interactive:
- self.connect("on-mouse-over", self.on_mouse_over)
- self.connect("on-click", self.on_clicked)
+ self.bar_color, self.label_color = None, None
self.connect("on-enter-frame", self.on_enter_frame)
- self.bars_selected = []
-
-
- def on_mouse_over(self, area, region):
- if region:
- self.mouse_bar = int(region[0])
- else:
- self.mouse_bar = None
-
- self.redraw()
-
- def on_clicked(self, area, bar):
- self.emit("bar-clicked", self.mouse_bar)
-
- def select_bar(self, index):
- pass
-
- def get_bar_color(self, index):
- # returns color darkened by it's index
- # the approach reduces contrast by each step
- base_color = self.bar_base_color or (220, 220, 220)
-
- base_hls = colorsys.rgb_to_hls(*base_color)
-
- step = (base_hls[1] - 30) / 10 #will go from base down to 20 and max 22 steps
-
- return colorsys.hls_to_rgb(base_hls[0],
- base_hls[1] - step * index,
- base_hls[2])
-
-
-
- def plot(self, keys, data, stack_keys = None):
- """Draw chart with given data"""
- self.keys, self.data, self.stack_keys = keys, data, stack_keys
-
- self.show()
-
- if not data: #if there is no data, just draw blank
- self.redraw()
- return
-
-
- min, self.max_value = get_limits(data)
-
- self._update_targets()
-
- if not self.animation:
- self.tweener.finish()
-
- self.redraw()
-
-
- def on_enter_frame(self, scene, context):
- # fill whole area
- if self.background:
- g = graphics.Graphics(context)
- g.fill_area(0, 0, self.width, self.height, self.background)
-
-
- def _update_targets(self):
- # calculates new factors and then updates existing set
- max_value = float(self.max_value) or 1 # avoid division by zero
-
- self.bars = size_list(self.bars, self.data)
-
- #need function to go recursive
- def retarget(bars, new_values):
- for i in range(len(new_values)):
- if isinstance(new_values[i], list):
- bars[i] = retarget(bars[i], new_values[i])
- else:
- if isinstance(bars[i], Bar) == False:
- bars[i] = Bar(new_values[i], 0)
- else:
- bars[i].value = new_values[i]
- self.tweener.kill_tweens(bars[i])
-
- self.tweener.add_tween(bars[i], size = bars[i].value / float(max_value))
- return bars
-
- retarget(self.bars, self.data)
-
-
- def longest_label(self, labels):
- """returns width of the longest label"""
- max_extent = 0
- for label in labels:
- self.layout.set_text(label)
- label_w, label_h = self.layout.get_pixel_size()
- max_extent = max(label_w + 5, max_extent)
-
- return max_extent
-
- def draw(self):
- logging.error("OMG OMG, not implemented!!!")
-
-
-class BarChart(Chart):
- def on_enter_frame(self, scene, context):
- Chart.on_enter_frame(self, scene, context)
-
- if not self.data:
- return
-
- g = graphics.Graphics(context)
-
- # TODO - should handle the layout business in graphics
- self.layout = context.create_layout()
- default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
- default_font.set_size(8 * pango.SCALE)
- self.layout.set_font_description(default_font)
-
- context.set_line_width(1)
-
-
- # determine graph dimensions
- if self.show_stack_labels:
- legend_width = self.legend_width or self.longest_label(self.keys)
- elif self.show_scale:
- if self.grid_stride < 1:
- grid_stride = int(self.max_value * self.grid_stride)
- else:
- grid_stride = int(self.grid_stride)
-
- scale_labels = [self.value_format % i
- for i in range(grid_stride, int(self.max_value), grid_stride)]
- self.legend_width = legend_width = self.legend_width or self.longest_label(scale_labels)
- else:
- legend_width = self.legend_width
+ if self.graph_interactive:
+ self.connect("on-mouse-over", self.on_mouse_over)
+ self.connect("on-mouse-out", self.on_mouse_out)
+ self.connect("on-click", self.on_click)
- if self.stack_keys and self.labels_at_end:
- self.graph_x = 0
- self.graph_width = self.width - legend_width
+ def find_colors(self):
+ bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
+ if self.colors.is_light(bg_color):
+ self.bar_color = self.colors.darker(bg_color, 30)
else:
- self.graph_x = legend_width + 8 # give some space to scale labels
- self.graph_width = self.width - self.graph_x - 10
-
- self.graph_y = 0
- self.graph_height = self.height - 15
-
- if self.chart_background:
- g.fill_area(self.graph_x, self.graph_y,
- self.graph_width, self.graph_height,
- self.chart_background)
-
- g.stroke()
-
- # bars and keys
- max_bar_size = self.graph_height
- #make sure bars don't hit the ceiling
- if self.animate or self.before_drag_animate:
- max_bar_size = self.graph_height - 10
-
-
- prev_label_end = None
- self.layout.set_width(-1)
-
- 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)))
+ self.bar_color = self.colors.darker(bg_color, -30)
# now for the text - we want reduced contrast for relaxed visuals
fg_color = self.get_style().fg[gtk.STATE_NORMAL].to_string()
if self.colors.is_light(fg_color):
- label_color = self.colors.darker(fg_color, 80)
+ self.label_color = self.colors.darker(fg_color, 80)
else:
- label_color = self.colors.darker(fg_color, -80)
+ self.label_color = self.colors.darker(fg_color, -80)
- for key, bar, data in zip(self.keys, self.bars, self.data):
- g.set_color(label_color);
- self.layout.set_text(key)
- label_w, label_h = self.layout.get_pixel_size()
+ def on_mouse_over(self, scene, targets):
+ bar = targets[0]
+ if bar.key not in self.selected_keys:
+ bar.fill = self.get_style().base[gtk.STATE_PRELIGHT].to_string()
- intended_x = exes[key][0] + (exes[key][1] - label_w) / 2
+ def on_mouse_out(self, scene, targets):
+ bar = targets[0]
+ if bar.key not in self.selected_keys:
+ bar.fill = self.bar_color
- if not prev_label_end or intended_x > prev_label_end:
- g.move_to(intended_x, self.graph_height + 4)
- context.show_layout(self.layout)
+ def on_click(self, scene, event, targets):
+ clicked_bar = targets[0]
+ self.emit("bar-clicked", clicked_bar.key)
- prev_label_end = intended_x + label_w + 3
+ def plot(self, keys, data):
+ bars = dict([(bar.key, bar.normalized) for bar in self.bars])
- bar_start = 0
+ max_val = float(max(data))
- # determine bar color
- base_color = self.bar_base_color
- if not base_color: #yay, we can be theme friendly!
- bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
- if self.colors.is_light(bg_color):
- base_color = self.colors.darker(bg_color, 30)
- else:
- base_color = self.colors.darker(bg_color, -30)
- tick_color = self.colors.darker(bg_color, -50)
-
- if self.stack_keys:
- 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]) or self.get_bar_color(j)
- g.fill_area(exes[key][0],
- self.graph_height - bar_start,
- exes[key][1],
- bar_size,
- last_color)
+ new_bars, new_labels = [], []
+ for key, value in zip(keys, data):
+ if max_val:
+ normalized = value / max_val
else:
- bar_size = round(max_bar_size * bar.size)
- bar_start = bar_size
+ normalized = 0
+ bar = Bar(key, self.value_format % value, normalized, self.label_color)
+ bar.interactive = self.graph_interactive
- last_color = self.key_colors.get(key) or base_color
- g.fill_area(exes[key][0],
- self.graph_y + self.graph_height - bar_size,
- exes[key][1],
- bar_size,
- last_color)
-
-
- if self.values_on_bars: # it is either stack labels or values at the end for now
- if self.stack_keys:
- total_value = sum(data[i])
- else:
- total_value = data[i]
+ if key in bars:
+ bar.normalized = bars[key]
+ self.tweener.add_tween(bar, normalized=normalized)
+ new_bars.append(bar)
- self.layout.set_width(-1)
- self.layout.set_text(self.value_format % total_value)
- label_w, label_h = self.layout.get_pixel_size()
+ label = graphics.Label(key, size = 8, alignment = pango.ALIGN_RIGHT)
+ new_labels.append(label)
- if bar_start > label_h + 2:
- label_y = self.graph_y + self.graph_height - bar_start + 5
- else:
- label_y = self.graph_y + self.graph_height - bar_start - label_h + 5
-
- 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 self.colors.is_light(last_color):
- g.set_color(label_color)
- else:
- g.set_color(self.colors.almost_white)
-
- context.show_layout(self.layout)
-
-
- #white grid and scale values
- if self.background:
- grid_color = self.background
- else:
- grid_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
-
- self.layout.set_width(-1)
- if self.grid_stride and self.max_value:
- # if grid stride is less than 1 then we consider it to be percentage
- if self.grid_stride < 1:
- grid_stride = int(self.max_value * self.grid_stride)
- else:
- grid_stride = int(self.grid_stride)
-
- context.set_line_width(1)
- for i in range(grid_stride, int(self.max_value), grid_stride):
- y = round(max_bar_size * (i / self.max_value)) + 0.5
-
- if self.show_scale:
- self.layout.set_text(self.value_format % i)
- label_w, label_h = self.layout.get_pixel_size()
- context.move_to(legend_width - label_w - 8,
- y - label_h / 2)
- g.set_color(self.colors.aluminium[4])
- context.show_layout(self.layout)
-
- g.set_color(grid_color)
- g.move_to(legend_width, y)
- g.line_to(self.width, y)
-
-
- #stack keys
- if self.show_stack_labels:
- #put series keys
- g.set_color(label_color);
-
- y = self.graph_height
- label_y = None
-
- # if labels are at end, then we need show them for the last bar!
- if self.labels_at_end:
- factors = self.bars[-1]
- else:
- factors = self.bars[0]
-
- if isinstance(factors, Bar):
- factors = [factors]
-
- self.layout.set_ellipsize(pango.ELLIPSIZE_END)
- self.layout.set_width(self.graph_x * pango.SCALE)
- if self.labels_at_end:
- self.layout.set_alignment(pango.ALIGN_LEFT)
- else:
- self.layout.set_alignment(pango.ALIGN_RIGHT)
-
- for j in range(len(factors)):
- factor = factors[j].size
- bar_size = factor * max_bar_size
-
- if round(bar_size) > 0 and self.stack_keys:
- label = "%s" % self.stack_keys[j]
-
-
- self.layout.set_text(label)
- label_w, label_h = self.layout.get_pixel_size()
-
- y -= bar_size
- intended_position = round(y + (bar_size - label_h) / 2)
-
- if label_y:
- label_y = min(intended_position, label_y - label_h)
- else:
- label_y = intended_position
-
- if self.labels_at_end:
- label_x = self.graph_x + self.graph_width
- line_x1 = self.graph_x + self.graph_width - 1
- line_x2 = self.graph_x + self.graph_width - 6
- else:
- label_x = -8
- line_x1 = self.graph_x - 6
- line_x2 = self.graph_x
+ for sprite in self.bars:
+ self.plot_area.sprites.remove(sprite)
+ for sprite in self.labels:
+ self.sprites.remove(sprite)
- context.move_to(label_x, label_y)
- context.show_layout(self.layout)
+ self.bars, self.labels = new_bars, new_labels
+ self.add_child(*self.labels)
+ self.plot_area.add_child(*self.bars)
- if label_y != intended_position:
- context.move_to(line_x1, label_y + label_h / 2)
- context.line_to(line_x2, round(y + bar_size / 2))
-
- context.stroke()
+ self.show()
+ self.redraw()
-class HorizontalBarChart(Chart):
def on_enter_frame(self, scene, context):
- Chart.on_enter_frame(self, scene, context)
- g = graphics.Graphics(context)
-
- if not self.data:
- return
-
- rowcount, keys = len(self.keys), self.keys
-
- # push graph to the right, so it doesn't overlap
- legend_width = self.legend_width or self.longest_label(keys)
-
- self.graph_x = legend_width
- self.graph_x += 8 #add another 8 pixes of padding
-
- self.graph_width = self.width - self.graph_x
- self.graph_y, self.graph_height = 0, self.height
+ self.find_colors()
+ self.plot_area.y = 0
+ self.plot_area.height = self.height - self.plot_area.y
+ self.plot_area.x = self.legend_width + 8
+ self.plot_area.width = self.width - self.plot_area.x
- if self.chart_background:
- g.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
-
-
- if not self.data: # go home if we have nothing
- return
-
- 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
-
- # TODO - should handle the layout business in graphics
- self.layout = context.create_layout()
- default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
- default_font.set_size(8 * pango.SCALE)
- self.layout.set_font_description(default_font)
-
-
- self.layout.set_alignment(pango.ALIGN_RIGHT)
- self.layout.set_ellipsize(pango.ELLIPSIZE_END)
-
-
-
- context.set_line_width(1)
-
- # bars and labels
- self.layout.set_width(legend_width * pango.SCALE)
-
-
- # determine bar color
- base_color = self.bar_base_color
- if not base_color: #yay, we can be theme friendly!
- bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
- if self.colors.is_light(bg_color):
- base_color = self.colors.darker(bg_color, 30)
- else:
- base_color = self.colors.darker(bg_color, -30)
- tick_color = self.colors.darker(bg_color, -50)
- last_color = base_color
-
-
- # now for the text - we want reduced contrast for relaxed visuals
- fg_color = self.get_style().fg[gtk.STATE_NORMAL].to_string()
- if self.colors.is_light(fg_color):
- label_color = self.colors.darker(fg_color, 80)
- else:
- label_color = self.colors.darker(fg_color, -80)
-
-
- for i, label in enumerate(keys):
- if 1 == 2 and self.interactive: # TODO - put interaction back
- self.register_mouse_region(0,
- positions[label][0],
- self.width,
- positions[label][0] + positions[label][1],
- str(i))
-
- self.layout.set_width(legend_width * pango.SCALE)
- self.layout.set_text(label)
- label_w, label_h = self.layout.get_pixel_size()
- label_y = positions[label][0] + (positions[label][1] - label_h) / 2
-
- if i == self.mouse_bar:
- g.set_color(self.get_style().fg[gtk.STATE_PRELIGHT])
- else:
- g.set_color(label_color)
-
-
- context.move_to(0, label_y)
- context.show_layout(self.layout)
-
-
- if self.stack_keys:
- bar_start = 0
-
- 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)
- g.fill_area(self.graph_x + bar_start,
- positions[label][0],
- bar_size,
- positions[label][1],
- last_color)
- bar_start += bar_size
- else:
- bar_size = round(max_bar_size * self.bars[i].size)
- bar_start = bar_size
-
- if i in self.bars_selected:
- last_color = self.get_style().bg[gtk.STATE_SELECTED].to_string()
- elif i == self.mouse_bar:
- last_color = self.get_style().base[gtk.STATE_PRELIGHT].to_string()
+ for i, (label, bar) in enumerate(zip(self.labels, self.bars)):
+ bar_width = min(round((self.plot_area.height - y) / (len(self.bars) - i)), self.max_width)
+ bar.y = y
+ bar.vertical = False
+ bar.height = bar_width
+ bar.width = self.plot_area.width
+
+ if bar.key in self.selected_keys:
+ bar.fill = self.get_style().bg[gtk.STATE_SELECTED].to_string()
+
+ if bar.normalized == 0:
+ bar.label.color = self.get_style().fg[gtk.STATE_SELECTED].to_string()
+ bar.label_background.fill = self.get_style().bg[gtk.STATE_SELECTED].to_string()
+ bar.label_background.visible = True
else:
- last_color = self.key_colors.get(self.keys[i]) or base_color
-
- g.fill_area(self.graph_x,
- positions[label][0],
- bar_size,
- positions[label][1],
- last_color)
-
-
- # value labels
- if self.stack_keys:
- total_value = sum(self.data[i])
- else:
- total_value = self.data[i]
-
- self.layout.set_width(-1)
- self.layout.set_text(self.value_format % total_value)
- label_w, label_h = self.layout.get_pixel_size()
-
- vertical_padding = max((positions[label][1] - label_h) / 2.0, 1)
-
- label_y = positions[label][0] + (positions[label][1] - label_h) / 2.0
- if bar_start - vertical_padding < label_w:
- label_x = self.graph_x + bar_start + vertical_padding
-
- # avoid zero selected bars without any hints
- if not self.stack_keys and i in self.bars_selected and self.bars[i].value == 0:
- g.set_color(self.get_style().bg[gtk.STATE_SELECTED])
- self.draw_rect(label_x - 2,
- label_y - 2,
- label_w + 4,
- label_h + 4, 4)
- g.fill()
- g.set_color(self.get_style().fg[gtk.STATE_SELECTED])
- else:
- g.set_color(label_color)
- else:
- label_x = self.graph_x + bar_start - label_w - vertical_padding
-
- if i in self.bars_selected:
- g.set_color(self.get_style().fg[gtk.STATE_SELECTED].to_string())
- else:
- # we are in the bar so make sure that the font color is distinguishable
- if self.colors.is_light(last_color):
- g.set_color(label_color)
+ bar.label_background.visible = False
+ if bar.label.x < round(bar.width * bar.normalized):
+ bar.label.color = self.get_style().fg[gtk.STATE_SELECTED].to_string()
else:
- g.set_color(self.colors.almost_white)
+ bar.label.color = self.label_color
+ if not bar.fill:
+ bar.fill = self.bar_color
- context.move_to(label_x, label_y)
- context.show_layout(self.layout)
+ bar.label.color = self.label_color
+ bar.label_background.fill = None
- context.stroke()
+ label.y = y + (bar_width - label.height) / 2 + self.plot_area.y
+ if not label.color:
+ label.width = self.legend_width
+ label.color = self.label_color
+ y += bar_width + 1
-class HorizontalDayChart(Chart):
+
+class HorizontalDayChart(graphics.Scene):
"""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)
+ def __init__(self, max_bar_width, legend_width):
+ graphics.Scene.__init__(self)
+ self.max_bar_width = max_bar_width
+ self.legend_width = legend_width
self.start_time, self.end_time = None, None
+ self.connect("on-enter-frame", self.on_enter_frame)
def plot_day(self, keys, data, start_time = None, end_time = None):
self.keys, self.data = keys, data
@@ -714,7 +225,8 @@ class HorizontalDayChart(Chart):
self.redraw()
def on_enter_frame(self, scene, context):
- Chart.on_enter_frame(self, scene, context)
+ g = graphics.Graphics(context)
+
rowcount, keys = len(self.keys), self.keys
start_hour = 0
@@ -733,16 +245,19 @@ class HorizontalDayChart(Chart):
self.graph_width = self.width - self.graph_x
+ # TODO - should handle the layout business in graphics
+ self.layout = context.create_layout()
+ default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+ default_font.set_size(8 * pango.SCALE)
+ self.layout.set_font_description(default_font)
+
+
#on the botttom leave some space for label
self.layout.set_text("1234567890:")
label_w, label_h = self.layout.get_pixel_size()
self.graph_y, self.graph_height = 0, self.height - label_h - 4
-
- if self.chart_background:
- g.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
return
@@ -779,14 +294,11 @@ class HorizontalDayChart(Chart):
factor = max_bar_size / float(end_hour - start_hour)
# determine bar color
- base_color = self.bar_base_color
- if not base_color: #yay, we can be theme friendly!
- bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
- if self.colors.is_light(bg_color):
- base_color = self.colors.darker(bg_color, 30)
- else:
- base_color = self.colors.darker(bg_color, -30)
- tick_color = self.colors.darker(bg_color, -50)
+ bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
+ if self.colors.is_light(bg_color):
+ base_color = self.colors.darker(bg_color, 30)
+ else:
+ base_color = self.colors.darker(bg_color, -30)
for i, label in enumerate(keys):
g.set_color(label_color)
@@ -819,10 +331,7 @@ class HorizontalDayChart(Chart):
last_position = positions[keys[-1]]
- if self.background:
- grid_color = self.background
- else:
- grid_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
+ grid_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
for i in range(start_hour + 60, end_hour, pace):
x = round((i - start_hour) * factor)
@@ -834,7 +343,7 @@ class HorizontalDayChart(Chart):
context.move_to(self.graph_x + x - label_w / 2,
last_position[0] + last_position[1] + 4)
- g.set_color(self.colors.aluminium[4])
+ g.set_color(label_color)
context.show_layout(self.layout)
@@ -845,58 +354,3 @@ class HorizontalDayChart(Chart):
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"]
- 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/hamster/overview_totals.py b/src/hamster/overview_totals.py
index d40b074..9a0e98c 100644
--- a/src/hamster/overview_totals.py
+++ b/src/hamster/overview_totals.py
@@ -47,30 +47,27 @@ class TotalsBox(gtk.VBox):
x_offset = 100 # align all graphs to the left edge
- self.category_chart = charting.HorizontalBarChart(max_bar_width = 20,
- legend_width = x_offset,
- value_format = "%.1f",
- interactive = True)
+ self.category_chart = charting.Chart(max_bar_width = 20,
+ legend_width = x_offset,
+ value_format = "%.1f")
self.category_chart.connect("bar-clicked", self.on_category_clicked)
self.selected_categories = []
self.category_sums = None
self.get_widget("totals_by_category").add(self.category_chart);
- self.activity_chart = charting.HorizontalBarChart(max_bar_width = 20,
- legend_width = x_offset,
- value_format = "%.1f",
- interactive = True)
+ self.activity_chart = charting.Chart(max_bar_width = 20,
+ legend_width = x_offset,
+ value_format = "%.1f")
self.activity_chart.connect("bar-clicked", self.on_activity_clicked)
self.selected_activities = []
self.activity_sums = None
self.get_widget("totals_by_activity").add(self.activity_chart);
- self.tag_chart = charting.HorizontalBarChart(max_bar_width = 20,
- legend_width = x_offset,
- value_format = "%.1f",
- interactive = True)
+ self.tag_chart = charting.Chart(max_bar_width = 20,
+ legend_width = x_offset,
+ value_format = "%.1f")
self.tag_chart.connect("bar-clicked", self.on_tag_clicked)
self.selected_tags = []
self.tag_sums = None
@@ -82,31 +79,32 @@ class TotalsBox(gtk.VBox):
self.report_chooser = None
- def on_category_clicked(self, widget, idx):
- if idx in self.category_chart.bars_selected:
- self.category_chart.bars_selected.remove(idx)
- self.selected_categories.remove(self.category_sums[0][idx])
+ def on_category_clicked(self, widget, key):
+ if key in self.category_chart.selected_keys:
+ self.category_chart.selected_keys.remove(key)
+ self.selected_categories.remove(key)
else:
- self.category_chart.bars_selected.append(idx)
- self.selected_categories.append(self.category_sums[0][idx])
+ self.category_chart.selected_keys.append(key)
+ self.selected_categories.append(key)
+
self.do_charts()
- def on_activity_clicked(self, widget, idx):
- if idx in self.activity_chart.bars_selected:
- self.activity_chart.bars_selected.remove(idx)
- self.selected_activities.remove(self.activity_sums[0][idx])
+ def on_activity_clicked(self, widget, key):
+ if key in self.activity_chart.selected_keys:
+ self.activity_chart.selected_keys.remove(key)
+ self.selected_activities.remove(key)
else:
- self.activity_chart.bars_selected.append(idx)
- self.selected_activities.append(self.activity_sums[0][idx])
+ self.activity_chart.selected_keys.append(key)
+ self.selected_activities.append(key)
self.do_charts()
- def on_tag_clicked(self, widget, idx):
- if idx in self.tag_chart.bars_selected:
- self.tag_chart.bars_selected.remove(idx)
- self.selected_tags.remove(self.tag_sums[0][idx])
+ def on_tag_clicked(self, widget, key):
+ if key in self.tag_chart.selected_keys:
+ self.tag_chart.selected_keys.remove(key)
+ self.selected_tags.remove(key)
else:
- self.tag_chart.bars_selected.append(idx)
- self.selected_tags.append(self.tag_sums[0][idx])
+ self.tag_chart.selected_keys.append(key)
+ self.selected_tags.append(key)
self.do_charts()
diff --git a/src/hamster/stats.py b/src/hamster/stats.py
index 2662d78..67cb045 100644
--- a/src/hamster/stats.py
+++ b/src/hamster/stats.py
@@ -97,29 +97,25 @@ class Stats(object):
year_box.show_all()
- self.chart_category_totals = charting.HorizontalBarChart(value_format = "%.1f",
- bars_beveled = False,
- max_bar_width = 20,
- legend_width = 70)
+ self.chart_category_totals = charting.Chart(value_format = "%.1f",
+ max_bar_width = 20,
+ legend_width = 70,
+ interactive = False)
self.get_widget("explore_category_totals").add(self.chart_category_totals)
- self.chart_weekday_totals = charting.HorizontalBarChart(value_format = "%.1f",
- bars_beveled = False,
- max_bar_width = 20,
- legend_width = 70)
+ self.chart_weekday_totals = charting.Chart(value_format = "%.1f",
+ max_bar_width = 20,
+ legend_width = 70,
+ interactive = False)
self.get_widget("explore_weekday_totals").add(self.chart_weekday_totals)
- self.chart_weekday_starts_ends = charting.HorizontalDayChart(bars_beveled = False,
- animate = False,
- max_bar_width = 20,
- legend_width = 70)
+ self.chart_weekday_starts_ends = charting.HorizontalDayChart(max_bar_width = 20,
+ legend_width = 70)
self.get_widget("explore_weekday_starts_ends").add(self.chart_weekday_starts_ends)
- self.chart_category_starts_ends = charting.HorizontalDayChart(bars_beveled = False,
- animate = False,
- max_bar_width = 20,
- legend_width = 70)
+ self.chart_category_starts_ends = charting.HorizontalDayChart(max_bar_width = 20,
+ legend_width = 70)
self.get_widget("explore_category_starts_ends").add(self.chart_category_starts_ends)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]