[hamster-applet] simplified bar charts



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]