utils/grid.py
changeset 12 917ba023c576
child 124 c0d0f7bebb73
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/utils/grid.py	Tue Aug 29 17:47:17 2006 +0200
@@ -0,0 +1,1053 @@
+#!/usr/bin/env python
+
+import cairo
+import sys
+import re
+import gtk
+
+
+
+class DataRange:
+    def __init__ (self, start = 0, end = 0, value = ''):
+        self.start = start
+        self.end = end
+        self.value = value
+class EventString:
+    def __init__ (self, at = 0, value = ''):
+        self.at = at
+        self.value = value
+class EventFloat:
+    def __init__ (self, at = 0, value = 0.0):
+        self.at = at
+        self.value = value
+class EventInt:
+    def __init__ (self, at = 0, value = 0.0):
+        self.at = at
+        self.value = value
+def ranges_cmp (a, b):
+    return a.start - b.start
+def events_cmp (a,b):
+    return a.at - b.at
+class TimelineDataRange:
+    def __init__ (self, name = ''):
+        self.name = name
+        self.ranges = []
+        return
+    def __search (self, key):
+        l = 0
+        u = len (self.ranges)-1
+        while l <= u:
+            i = int ((l+u)/2)
+            if key >= self.ranges[i].start and key <= self.ranges[i].end:
+                return i
+            elif key < self.ranges[i].start:
+                u = i - 1
+            else:
+                # key > self.ranges[i].end
+                l = i + 1
+        return -1
+    def add_range (self, range):
+        self.ranges.append (range)
+    def get_all (self):
+        return self.ranges
+    def get_ranges (self, start, end):
+        s = self.__search (start)
+        e = self.__search (end)
+        if s == -1 and e == -1:
+            return []
+        elif s == -1:
+            return self.ranges[0:e+1]
+        elif e == -1:
+            return self.ranges[s:len (self.ranges)]
+        else:
+            return self.ranges[s:e+1]
+    def get_ranges_bounds (self, start, end):
+        s = self.__search (start)
+        e = self.__search (end)
+        if s == -1 and e == -1:
+            return (0,0)
+        elif s == -1:
+            return (0,e+1)
+        elif e == -1:
+            return (s, len (self.ranges))
+        else:
+            return (s,e+1)
+    def sort (self):
+        self.ranges.sort (ranges_cmp)
+    def get_bounds (self):
+        if len (self.ranges) > 0:
+            lo = self.ranges[0].start
+            hi = self.ranges[len (self.ranges)-1].end
+            return (lo, hi)
+        else:
+            return (0,0)
+class TimelineEvent:
+    def __init__ (self, name = ''):
+        self.name = name
+        self.events = []
+    def __search (self, key):
+        l = 0
+        u = len (self.events)-1
+        while l <= u:
+            i = int ((l+u)/2)
+            if key == self.events[i].at:
+                return i
+            elif key < self.events[i].at:
+                u = i - 1
+            else:
+                # key > self.events[i].at
+                l = i + 1
+        return l
+    def add_event (self, event):
+        self.events.append (event)
+    def get_events (self, start, end):
+        s = self.__search (start)
+        e = self.__search (end)
+        return self.events[s:e+1]
+    def get_events_bounds (self, start, end):
+        s = self.__search (start)
+        e = self.__search (end)
+        return (s, e+1)
+    def sort (self):
+        self.events.sort (events_cmp)
+    def get_bounds (self):
+        if len (self.events) > 0:
+            lo = self.events[0].at
+            hi = self.events[-1].at
+            return (lo,hi)
+        else:
+            return (0,0)
+
+class Timeline:
+    def __init__ (self, name = ''):
+        self.ranges = []
+        self.event_str = []
+        self.event_int = []
+        self.name = name
+    def get_range (self, name):
+        for range in self.ranges:
+            if range.name == name:
+                return range
+        timeline = TimelineDataRange (name)
+        self.ranges.append (timeline)
+        return timeline
+    def get_event_str (self, name):
+        for event_str in self.event_str:
+            if event_str.name == name:
+                return event_str
+        timeline = TimelineEvent (name)
+        self.event_str.append (timeline)
+        return timeline
+    def get_event_int (self, name):
+        for event_int in self.event_int:
+            if event_int.name == name:
+                return event_int
+        timeline = TimelineEvent (name)
+        self.event_int.append (timeline)
+        return timeline
+    def get_ranges (self):
+        return self.ranges
+    def get_events_str (self):
+        return self.event_str
+    def get_events_int (self):
+        return self.event_int
+    def sort (self):
+        for range in self.ranges:
+            range.sort ()
+        for event in self.event_int:
+            event.sort ()
+        for event in self.event_str:
+            event.sort ()
+    def get_bounds (self):
+        lo = 0
+        hi = 0
+        for range in self.ranges:
+            (range_lo, range_hi) = range.get_bounds ()
+            if range_lo < lo:
+                lo = range_lo
+            if range_hi > hi:
+                hi = range_hi
+        for event_str in self.event_str:
+            (ev_lo, ev_hi) = event_str.get_bounds ()
+            if ev_lo < lo:
+                lo = ev_lo
+            if ev_hi > hi:
+                hi = ev_hi
+        for event_int in self.event_int:
+            (ev_lo, ev_hi) = event_int.get_bounds ()
+            if ev_lo < lo:
+                lo = ev_lo
+            if ev_hi > hi:
+                hi = ev_hi
+        return (lo, hi)
+class Timelines:
+    def __init__ (self):
+        self.timelines = []
+    def get (self, name):
+        for timeline in self.timelines:
+            if timeline.name == name:
+                return timeline
+        timeline = Timeline (name)
+        self.timelines.append (timeline)
+        return timeline
+    def get_all (self):
+        return self.timelines
+    def sort (self):
+        for timeline in self.timelines:
+            timeline.sort ()
+    def get_bounds (self):
+        lo = 0
+        hi = 0
+        for timeline in self.timelines:
+            (t_lo, t_hi) = timeline.get_bounds ()
+            if t_lo < lo:
+                lo = t_lo
+            if t_hi > hi:
+                hi = t_hi
+        return (lo, hi)
+    def get_all_range_values (self):
+        range_values = {}
+        for timeline in self.timelines:
+            for ranges in timeline.get_ranges ():
+                for ran in ranges.get_all ():
+                    range_values[ran.value] = 1
+        return range_values.keys ()
+class Color:
+    def __init__ (self, r = 0.0, g = 0.0, b = 0.0):
+        self.r = r
+        self.g = g
+        self.b = b
+    def set (self, r, g, b):
+        self.r = r
+        self.g = g
+        self.b = b
+class Colors:
+    # XXX add more
+    default_colors = [Color (1,0,0), Color (0,1,0), Color (0,0,1),Color (1,1,0), Color(1,0,1), Color (0,1,1)]
+    def __init__ (self):
+        self.__colors = {}
+    def add (self, name, color):
+        self.__colors[name] = color
+    def lookup (self, name):
+        if not self.__colors.has_key (name):
+            self.add (name, self.default_colors.pop ())
+        return self.__colors.get(name)
+
+
+class TopLegendRenderer:
+    def __init__ (self):
+        self.__padding = 10
+    def set_padding (self, padding):
+        self.__padding = padding
+    def set_legends (self, legends, colors):
+        self.__legends = legends
+        self.__colors = colors
+    def layout (self, width):
+        self.__width = width
+        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1,1)
+        ctx = cairo.Context(surface)
+        line_height = 0
+        total_height = self.__padding
+        line_used = self.__padding
+        for legend in self.__legends:
+            (t_width, t_height) = ctx.text_extents (legend)[2:4]
+            item_width = self.__padding +  self.__padding + t_width +  self.__padding
+            item_height = t_height + self.__padding
+            if item_height > line_height:
+                line_height = item_height
+            if line_used + item_width > self.__width:
+                line_used = self.__padding + item_width
+                total_height += line_height
+            else:
+                line_used += item_width
+            x = line_used - item_width
+        total_height += line_height
+        self.__height = total_height
+            
+    def get_height (self):
+        return self.__height
+    def draw (self, ctx):
+        i = 0
+        line_height = 0
+        total_height = self.__padding
+        line_used = self.__padding
+        for legend in self.__legends:
+            (t_width, t_height) = ctx.text_extents (legend)[2:4]
+            item_width = self.__padding +  self.__padding + t_width +  self.__padding
+            item_height = t_height + self.__padding
+            if item_height > line_height:
+                line_height = item_height
+            if line_used + item_width > self.__width:
+                line_used = self.__padding + item_width
+                total_height += line_height
+            else:
+                line_used += item_width
+            x = line_used - item_width
+            ctx.rectangle (x, total_height, self.__padding, self.__padding)
+            ctx.set_source_rgb (0,0,0)
+            ctx.set_line_width (2)
+            ctx.stroke_preserve ()
+            ctx.set_source_rgb (self.__colors[i].r,
+                                self.__colors[i].g,
+                                self.__colors[i].b)
+            ctx.fill ()
+            ctx.move_to (x+self.__padding*2, total_height+t_height)
+            ctx.set_source_rgb (0,0,0)
+            ctx.show_text (legend)
+            i += 1
+
+        return
+
+class TimelinesRenderer:
+    def __init__ (self):
+        self.padding = 10
+        return
+    def get_height (self):
+        return self.height
+    def set_timelines (self, timelines, colors):
+        self.timelines = timelines
+        self.colors = colors
+    def set_render_range (self, start, end):
+        self.start = start
+        self.end = end
+    def get_data_x_start (self):
+        return self.padding / 2 + self.left_width + self.padding + self.right_width + self.padding/2
+    def layout (self, width):
+        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1,1)
+        ctx = cairo.Context(surface)
+        max_text_height = ctx.text_extents ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789")[3]
+
+        left_width = 0
+        right_width = 0
+        left_n_lines = 0
+        range_n = 0
+        eventint_n = 0
+        eventstr_n = 0
+        for timeline in self.timelines.get_all ():
+            left_n_lines += 1
+            t_width = ctx.text_extents (timeline.name)[2]
+            left_width = max (left_width, t_width)
+            for rang in timeline.get_ranges ():
+                t_width = ctx.text_extents (rang.name)[2]
+                right_width = max (right_width, t_width)
+                range_n += 1
+            for events_int in timeline.get_events_int ():
+                t_width = ctx.text_extents (events_int.name)[2]
+                right_width = max (right_width, t_width)
+                eventint_n += 1
+            for events_str in timeline.get_events_str ():
+                t_width = ctx.text_extents (events_str.name)[2]
+                right_width = max (right_width, t_width)
+                eventstr_n += 1
+
+        left_height = left_n_lines * max_text_height + (left_n_lines - 1) * self.padding
+        right_n_lines = range_n + eventint_n + eventstr_n
+        right_height = (right_n_lines - 1) * self.padding + right_n_lines * max_text_height
+        right_data_height = (eventint_n + eventstr_n) * (max_text_height + 5) + range_n * 10
+        right_data_height += (right_n_lines - 1) * self.padding
+
+        height = max (left_height, right_height)
+        height = max (height, right_data_height)
+
+        self.left_width = left_width
+        self.right_width = right_width
+        self.max_text_height = max_text_height
+        self.width = width
+        self.height = height + self.padding
+    def draw_line (self, ctx, x, y, width, height):
+        ctx.move_to (x, y)
+        ctx.rel_line_to (width, height)
+        ctx.close_path ()
+        ctx.set_operator (cairo.OPERATOR_SOURCE)
+        ctx.set_line_width (1.0)
+        ctx.set_source_rgb (0,0,0)
+        ctx.stroke ()
+    def draw_events (self, ctx, events, x, y, width, height):
+        if (self.grey_background % 2) == 0:
+            ctx.rectangle (x, y-self.padding/2,
+                           width, height+self.padding)
+            ctx.set_source_rgb (0.9,0.9,0.9)
+            ctx.fill ()
+        last_x_drawn = int (x)
+        (lo, hi) = events.get_events_bounds (self.start, self.end)
+        for event in events.events[lo:hi]:
+            real_x = int (x + (event.at - self.start) * width / (self.end - self.start))
+            if real_x > last_x_drawn+2:
+                ctx.rectangle (real_x, y, 1, 1)
+                ctx.set_source_rgb (1,0,0)
+                ctx.stroke ()
+                ctx.move_to (real_x, y+self.max_text_height)
+                ctx.set_source_rgb (0,0,0)
+                ctx.show_text (str (event.value))
+                last_x_drawn = real_x
+        self.grey_background += 1
+    def draw_ranges (self, ctx, ranges, x, y, width, height):
+        if (self.grey_background % 2) == 0:
+            ctx.rectangle (x, y-self.padding/2,
+                           width, height+self.padding)
+            ctx.set_source_rgb (0.9,0.9,0.9)
+            ctx.fill ()
+        last_x_drawn = int (x-1)
+        (lo, hi) = ranges.get_ranges_bounds (self.start, self.end)
+        for data_range in ranges.ranges[lo:hi]:
+            s = max (data_range.start, self.start)
+            e = min (data_range.end, self.end)
+            x_start = int (x + (s - self.start) * width / (self.end - self.start))
+            x_end = int (x + (e - self.start) * width / (self.end - self.start))
+            if x_end > last_x_drawn:
+                ctx.rectangle (x_start, y, x_end - x_start, 10)
+                ctx.set_source_rgb (0,0,0)
+                ctx.stroke_preserve ()
+                color = self.colors.lookup (data_range.value)
+                ctx.set_source_rgb (color.r, color.g, color.b)
+                ctx.fill ()
+                last_x_drawn = x_end
+
+        self.grey_background += 1
+        
+    def draw (self, ctx):
+        timeline_top = 0
+        top_y = self.padding / 2
+        left_x_start = self.padding / 2
+        left_x_end = left_x_start + self.left_width
+        right_x_start = left_x_end + self.padding
+        right_x_end = right_x_start + self.right_width
+        data_x_start = right_x_end + self.padding /2
+        data_x_end = self.width
+        data_width = data_x_end - data_x_start
+        cur_y = top_y
+        self.draw_line (ctx, 0, 0, self.width, 0)
+        self.grey_background = 1
+        for timeline in self.timelines.get_all ():
+            (y_bearing,t_width,t_height) = ctx.text_extents (timeline.name)[1:4]
+            ctx.move_to (left_x_start, cur_y + self.max_text_height - (t_height+y_bearing))
+            ctx.show_text (timeline.name);
+            for events_int in timeline.get_events_int ():
+                (y_bearing, t_width, t_height) = ctx.text_extents (events_int.name)[1:4]
+                ctx.move_to (right_x_start, cur_y + self.max_text_height - (t_height+y_bearing))
+                ctx.show_text (events_int.name)
+                self.draw_events (ctx, events_int, data_x_start, cur_y, data_width, self.max_text_height+5)
+                cur_y += self.max_text_height + 5 + self.padding
+                self.draw_line (ctx, right_x_start-self.padding/2, cur_y - self.padding / 2,
+                                self.right_width + self.padding, 0)
+
+            for events_str in timeline.get_events_str ():
+                (y_bearing, t_width, t_height) = ctx.text_extents (events_str.name)[1:4]
+                ctx.move_to (right_x_start, cur_y + self.max_text_height - (t_height+y_bearing))
+                ctx.show_text (events_str.name)
+                self.draw_events (ctx, events_str, data_x_start, cur_y, data_width, self.max_text_height+5)
+                cur_y += self.max_text_height + 5 + self.padding
+                self.draw_line (ctx, right_x_start-self.padding/2, cur_y - self.padding / 2,
+                                self.right_width + self.padding, 0)
+            for ranges in timeline.get_ranges ():
+                (y_bearing, t_width, t_height) = ctx.text_extents (ranges.name)[1:4]
+                ctx.move_to (right_x_start, cur_y + self.max_text_height - (t_height+y_bearing))
+                ctx.show_text (ranges.name)
+                self.draw_ranges (ctx, ranges, data_x_start, cur_y, data_width, 10)
+                cur_y += self.max_text_height + self.padding
+                self.draw_line (ctx, right_x_start-self.padding/2, cur_y - self.padding / 2,
+                                self.right_width + self.padding, 0)
+            self.draw_line (ctx, 0, cur_y - self.padding / 2,
+                            self.width, 0)
+        bot_y = cur_y - self.padding / 2
+        self.draw_line (ctx, left_x_end+self.padding/2, 0,
+                        0, bot_y)
+        self.draw_line (ctx, right_x_end+self.padding/2, 0,
+                        0, bot_y)
+        return
+
+class ScaleRenderer:
+    def __init__ (self):
+        self.__top = 0
+        return
+    def set_bounds (self, lo, hi):
+        self.__lo = lo
+        self.__hi = hi
+    def get_position (self, x):
+        real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
+        return real_x
+    def set_top (self):
+        self.__top = 1
+    def set_bot (self):
+        self.__top = 0
+    def layout (self, width):
+        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1,1)
+        ctx = cairo.Context(surface)
+
+        # calculate scale delta
+        data_delta = self.__hi - self.__lo
+        closest = 1
+        while (closest*10) < data_delta:
+            closest *= 10
+        if (data_delta / closest) == 0:
+            delta = closest
+        elif (data_delta / closest) == 1:
+            delta = closest / 10
+        else:
+            delta = closest
+        start = self.__lo - (self.__lo % delta) + delta
+        end = self.__hi - (self.__hi % delta)
+
+        self.__delta = delta
+        self.__width = width
+
+        # calculate text height
+        max_text_height = ctx.text_extents ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789")[3]
+        self.max_text_height = max_text_height
+        height = max_text_height + 10
+        self.__height = height
+    
+    def get_height (self):
+        return self.__height
+    def draw (self, ctx):
+        delta = self.__delta
+        start = self.__lo - (self.__lo % delta) + delta
+        end = self.__hi - (self.__hi % delta)
+
+        if self.__top == 1:
+            s = -1
+        else:
+            s = 1
+        # print scale points
+        ctx.set_source_rgb (0, 0, 0)
+        ctx.set_line_width (1.0)
+        ticks = range (int (start), int (end + delta), int (delta))
+        for x in ticks:
+            real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
+            ctx.move_to (real_x, 0)
+            ctx.line_to (real_x, 5*s)
+            ctx.close_path ()
+            ctx.stroke ()
+            (t_y_bearing, t_width, t_height) = ctx.text_extents (str (x))[1:4]
+            if self.__top:
+                text_delta = t_height + t_y_bearing
+            else:
+                text_delta = -t_y_bearing
+            ctx.move_to (real_x - t_width/2, (5 + 5 + text_delta)*s)
+            ctx.show_text (str (x))
+        # draw subticks
+        delta /= 10
+        if delta > 0:
+            start = self.__lo - (self.__lo % delta) + delta
+            end = self.__hi - (self.__hi % delta)
+            for x in range (int (start), int (end + delta), int (delta)):
+                real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
+                ctx.move_to (real_x, 0)
+                ctx.line_to (real_x, 3*s)
+                ctx.close_path ()
+                ctx.stroke ()
+        
+        
+
+class GraphicRenderer:
+    def __init__(self, start, end):
+        self.__start = float (start)
+        self.__end = float (end)
+        self.__mid_scale = ScaleRenderer ()
+        self.__mid_scale.set_top ()
+        self.__bot_scale = ScaleRenderer ()
+        self.__bot_scale.set_bounds (start, end)
+        self.__bot_scale.set_bot ()
+        self.__width = 1
+        self.__height = 1
+    def get_width (self):
+        return self.__width
+    def get_height (self):
+        return self.__height
+    # return x, y, width, height
+    def get_data_rectangle (self):
+        y_start = self.__top_legend.get_height ()
+        x_start = self.__data.get_data_x_start ()
+        return (x_start, y_start, self.__width - x_start, self.__data.get_height ())
+    def scale_data (self, x):
+        x_start = self.__data.get_data_x_start ()
+        x_scaled = x / (self.__width - x_start) * (self.__r_end - self.__r_start)
+        return x_scaled
+    # return x, y, width, height
+    def get_selection_rectangle (self):
+        y_start = self.__top_legend.get_height () + self.__data.get_height () + self.__mid_scale.get_height () + 20
+        y_height = self.__bot_scale.get_height () + 20
+        x_start = self.__bot_scale.get_position (self.__r_start)
+        x_end = self.__bot_scale.get_position (self.__r_end)
+        return (x_start,y_start,x_end-x_start,y_height)
+    def scale_selection (self, x):
+        x_scaled = x / self.__width * (self.__end - self.__start)
+        return x_scaled
+    def set_range (self,start, end):
+        s = min (start, end)
+        e = max (start, end)
+        start = max (self.__start, s)
+        end = min (self.__end, e)
+        self.__r_start = start
+        self.__r_end = end
+        self.__data.set_render_range (start, end)
+        self.__mid_scale.set_bounds (start, end)
+        self.layout (self.__width, self.__height)
+    def get_range (self):
+        return (self.__r_start, self.__r_end)
+    def set_data (self, data):
+        self.__data = data
+    def set_top_legend (self, top_legend):
+        self.__top_legend = top_legend
+    def layout (self, width, height):
+        self.__width = width
+        self.__height = height
+        self.__top_legend.layout (width)
+        top_legend_height = self.__top_legend.get_height ()
+        self.__data.layout (width)
+        self.__mid_scale.layout (width - self.__data.get_data_x_start ())
+        self.__bot_scale.layout (width)
+        return
+    def __x_pixel (self, x, width):
+        new_x = (x - self.__start) * width / (self.__end - self.__start)
+        return new_x
+    
+    def draw (self, ctx):
+        # default background is white
+        ctx.save ()
+        ctx.set_source_rgb (1, 1, 1)
+        ctx.set_operator (cairo.OPERATOR_SOURCE)
+        ctx.rectangle (0,0,self.__width,self.__height)
+        ctx.fill ()
+
+        # top legend 
+        ctx.save ()
+        self.__top_legend.draw (ctx)
+        top_legend_height = self.__top_legend.get_height ()
+        ctx.restore ()
+
+        # separation line
+        ctx.move_to (0, top_legend_height)
+        ctx.line_to (self.__width, top_legend_height)
+        ctx.close_path ()
+        ctx.set_line_width (2)
+        ctx.set_source_rgb (0,0,0)
+        ctx.stroke ()
+
+        # data
+        ctx.save ()
+        ctx.translate (0,
+                       top_legend_height)
+        self.__data.draw (ctx)
+        ctx.restore ()
+
+        # scale below data
+        ctx.save ()
+        ctx.translate (self.__data.get_data_x_start (),
+                       top_legend_height + self.__data.get_height () + self.__mid_scale.get_height ())
+        self.__mid_scale.draw (ctx)
+        ctx.restore ()
+
+        height_used = top_legend_height + self.__data.get_height () + self.__mid_scale.get_height ()
+
+        # separation between scale and left pane
+        ctx.move_to (self.__data.get_data_x_start (), height_used)
+        ctx.rel_line_to (0, -self.__mid_scale.get_height ())
+        ctx.close_path ()
+        ctx.set_source_rgb (0,0,0)
+        ctx.set_line_width (2)
+        ctx.stroke ()
+
+        # separation below scale
+        ctx.move_to (0, height_used)
+        ctx.line_to (self.__width, height_used)
+        ctx.close_path ()
+        ctx.set_line_width (2)
+        ctx.set_source_rgb (0,0,0)
+        ctx.stroke ()
+
+        select_start = self.__bot_scale.get_position (self.__r_start)
+        select_end = self.__bot_scale.get_position (self.__r_end)
+
+        # left connection between top scale and bottom scale
+        ctx.move_to (0, height_used);
+        ctx.line_to (self.__data.get_data_x_start (), height_used)
+        ctx.line_to (select_start, height_used + 20)
+        ctx.line_to (0, height_used + 20)
+        ctx.line_to (0,height_used)
+        ctx.set_source_rgb (0,0,0)
+        ctx.set_line_width (1)
+        ctx.stroke_preserve ()
+        ctx.set_source_rgb (0.9,0.9,0.9)
+        ctx.fill ()
+
+        # right connection between top scale and bottom scale
+        ctx.move_to (self.__width, height_used)
+        ctx.line_to (self.__width, height_used+20)
+        ctx.line_to (select_end, height_used+20)
+        ctx.line_to (self.__width, height_used)
+        ctx.set_source_rgb (0,0,0)
+        ctx.set_line_width (1)
+        ctx.stroke_preserve ()
+        ctx.set_source_rgb (0.9,0.9,0.9)
+        ctx.fill ()
+
+        height_used += 20
+
+        # unused area background
+        unused_start = self.__bot_scale.get_position (self.__r_start)
+        unused_end = self.__bot_scale.get_position (self.__r_end)
+        unused_height = self.__bot_scale.get_height () + 20
+        ctx.rectangle (0, height_used,
+                       unused_start,
+                       unused_height)
+        ctx.rectangle (unused_end,
+                       height_used,
+                       self.__width - unused_end,
+                       unused_height)
+        ctx.set_source_rgb (0.9,0.9,0.9)
+        ctx.fill ()        
+
+        # border line around bottom scale
+        ctx.move_to (unused_end, height_used)
+        ctx.line_to (self.__width, height_used)
+        ctx.line_to (self.__width, height_used + unused_height)
+        ctx.line_to (0, height_used + unused_height)
+        ctx.line_to (0, height_used)
+        ctx.line_to (unused_start, height_used)
+        ctx.close_path ()
+        ctx.set_line_width (2)
+        ctx.set_source_rgb (0,0,0)
+        ctx.stroke ()
+        ctx.move_to (unused_start, height_used)
+        ctx.line_to (unused_end, height_used)
+        ctx.close_path ()
+        ctx.set_line_width (1)
+        ctx.set_source_rgb (0.9,0.9,0.9)
+        ctx.stroke ()
+
+        # unused area dot borders
+        ctx.save ()
+        ctx.move_to (max (unused_start, 2), height_used)
+        ctx.rel_line_to (0,unused_height)
+        ctx.move_to (min (unused_end, self.__width-2), height_used)
+        ctx.rel_line_to (0, unused_height)
+        ctx.set_dash ([5], 0)
+        ctx.set_source_rgb (0,0,0)
+        ctx.set_line_width (1)
+        ctx.stroke ()
+        ctx.restore ()
+
+        # bottom scale
+        ctx.save ()
+        ctx.translate (0, height_used)
+        self.__bot_scale.draw (ctx)
+        ctx.restore ()
+
+class GtkGraphicRenderer (gtk.DrawingArea):
+    def __init__ (self, data):
+        super (GtkGraphicRenderer, self).__init__ ()
+        self.__data = data
+        self.__moving_left = False
+        self.__moving_right = False
+        self.__moving_both = False
+        self.__moving_top = False
+        self.__force_full_redraw = True
+        self.add_events (gtk.gdk.POINTER_MOTION_MASK)
+        self.add_events (gtk.gdk.BUTTON_PRESS_MASK)
+        self.add_events (gtk.gdk.BUTTON_RELEASE_MASK)
+        self.connect ("expose_event", self.expose)
+        self.connect ('size-allocate', self.size_allocate)
+        self.connect ('motion-notify-event', self.motion_notify)
+        self.connect ('button-press-event', self.button_press)
+        self.connect ('button-release-event', self.button_release)
+    def set_smaller_zoom (self):
+        (start, end) = self.__data.get_range ()
+        self.__data.set_range (start, start+(end-start)*2)
+        self.__force_full_redraw = True
+        self.queue_draw ()
+    def set_bigger_zoom (self):
+        (start, end) = self.__data.get_range ()
+        self.__data.set_range (start, start+(end-start)/2)
+        self.__force_full_redraw = True
+        self.queue_draw ()
+    def output_png (self, filename):
+        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
+                                     self.__data.get_width (),
+                                     self.__data.get_height ())
+        ctx = cairo.Context (self.__buffer_surface)
+        self.__data.draw (ctx)
+        surface.write_to_png (filename)
+    def button_press (self, widget, event):
+        (x, y, width, height) = self.__data.get_selection_rectangle ()
+        (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle ()
+        if event.y > y and event.y < y+height:
+            if abs (event.x - x) < 5:
+                self.__moving_left = True
+                return True
+            if abs (event.x - (x+width)) < 5:
+                self.__moving_right = True
+                return True
+            if event.x > x and event.x < x+width:
+                self.__moving_both = True
+                self.__moving_both_start = event.x
+                self.__moving_both_cur = event.x
+                return True
+        if event.y > d_y and event.y < (d_y + d_height):
+            if event.x > d_x and event.x < (d_x + d_width):
+                self.__moving_top = True
+                self.__moving_top_start = event.x
+                self.__moving_top_cur = event.x
+                return True
+        return False
+    def button_release (self, widget, event):
+        if self.__moving_left:
+            self.__moving_left = False
+            left = self.__data.scale_selection (self.__moving_left_cur)
+            right = self.__data.get_range ()[1]
+            self.__data.set_range (left, right)
+            self.__force_full_redraw = True
+            self.queue_draw ()
+            return True
+        if self.__moving_right:
+            self.__moving_right = False
+            right = self.__data.scale_selection (self.__moving_right_cur)
+            left = self.__data.get_range ()[0]
+            self.__data.set_range (left, right)
+            self.__force_full_redraw = True
+            self.queue_draw ()
+            return True
+        if self.__moving_both:
+            self.__moving_both = False
+            delta = self.__data.scale_selection (self.__moving_both_cur - self.__moving_both_start)
+            (left, right) = self.__data.get_range ()
+            self.__data.set_range (left+delta, right+delta)
+            self.__force_full_redraw = True
+            self.queue_draw ()
+            return True
+        if self.__moving_top:
+            self.__moving_top = False
+        return False
+    def motion_notify (self, widget, event):
+        (x, y, width, height) = self.__data.get_selection_rectangle ()
+        if self.__moving_left:
+            if event.x <= 0:
+                self.__moving_left_cur = 0
+            elif event.x >= x+width:
+                self.__moving_left_cur = x+width
+            else:
+                self.__moving_left_cur = event.x
+            self.queue_draw_area (0, int(y), int(self.__width), int(height))
+            return True
+        if self.__moving_right:
+            if event.x >= self.__width:
+                self.__moving_right = self.__width
+            elif event.x < x:
+                self.__moving_right_cur = x
+            else:
+                self.__moving_right_cur = event.x
+            self.queue_draw_area (0, int(y), int(self.__width), int(height))
+            return True
+        if self.__moving_both:
+            cur_e = self.__width - (x + width - self.__moving_both_start)
+            cur_s = (self.__moving_both_start - x)
+            if event.x < cur_s:
+                self.__moving_both_cur = cur_s
+            elif event.x > cur_e:
+                self.__moving_both_cur = cur_e
+            else:
+                self.__moving_both_cur = event.x
+            self.queue_draw_area (0, int(y), int(self.__width), int(height))
+            return True
+        if self.__moving_top:
+            self.__moving_top_cur = event.x
+            delta = self.__data.scale_data (self.__moving_top_start-self.__moving_top_cur)
+            (left, right) = self.__data.get_range ()
+            self.__data.set_range (left+delta, right+delta)
+            self.__force_full_redraw = True
+            self.__moving_top_start = event.x
+            self.queue_draw ()
+            return True
+        (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle ()
+        if event.y > y and event.y < y+height:
+            if abs (event.x - x) < 5 or abs (event.x - (x+width)) < 5:
+                widget.window.set_cursor (gtk.gdk.Cursor (gtk.gdk.SB_H_DOUBLE_ARROW))
+                return True
+            if event.x > x and event.x < x+width:
+                widget.window.set_cursor (gtk.gdk.Cursor (gtk.gdk.FLEUR))
+                return True
+        if event.y > d_y and event.y < (d_y + d_height):
+            if event.x > d_x and event.x < (d_x + d_width):
+                widget.window.set_cursor (gtk.gdk.Cursor (gtk.gdk.FLEUR))
+                return True
+        widget.window.set_cursor (None)
+        return False
+    def size_allocate (self, widget, allocation):
+        self.__width = allocation.width
+        self.__height = allocation.height
+        self.__data.layout (allocation.width, allocation.height)
+        self.__force_full_redraw = True
+        self.queue_draw ()
+    def expose (self, widget, event):
+        if self.__force_full_redraw:
+            self.__buffer_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
+                                                       self.__data.get_width (),
+                                                       self.__data.get_height ())
+            ctx = cairo.Context(self.__buffer_surface)
+            self.__data.draw (ctx)
+            self.__force_full_redraw = False
+        ctx = widget.window.cairo_create()
+        ctx.rectangle(event.area.x, event.area.y,
+                      event.area.width, event.area.height)
+        ctx.clip()
+        ctx.set_source_surface (self.__buffer_surface)
+        ctx.paint ()
+        (x, y, width, height) = self.__data.get_selection_rectangle ()
+        if self.__moving_left:
+            ctx.move_to (max (self.__moving_left_cur, 2), y)
+            ctx.rel_line_to (0, height)
+            ctx.close_path ()
+            ctx.set_line_width (1)
+            ctx.set_source_rgb (0,0,0)
+            ctx.stroke ()
+        if self.__moving_right:
+            ctx.move_to (min (self.__moving_right_cur, self.__width-2), y)
+            ctx.rel_line_to (0, height)
+            ctx.close_path ()
+            ctx.set_line_width (1)
+            ctx.set_source_rgb (0,0,0)
+            ctx.stroke ()
+        if self.__moving_both:
+            delta_x = self.__moving_both_cur - self.__moving_both_start
+            left_x = x + delta_x
+            ctx.move_to (x+delta_x, y)
+            ctx.rel_line_to (0, height)
+            ctx.close_path ()
+            ctx.move_to (x+width+delta_x, y)
+            ctx.rel_line_to (0, height)
+            ctx.close_path ()
+            ctx.set_source_rgb (0,0,0)
+            ctx.set_line_width (1)
+            ctx.stroke ()
+        return False
+
+class MainWindow:
+    def __init__ (self):
+        return
+    def run (self, graphic):
+        window = gtk.Window()
+        self.__window = window
+        window.set_default_size (200, 200)
+        vbox = gtk.VBox ()
+        window.add (vbox)
+        render = GtkGraphicRenderer(graphic)
+        self.__render = render
+        vbox.pack_end (render, True, True, 0)
+        hbox = gtk.HBox ()
+        vbox.pack_start (hbox, False, False, 0)
+        smaller_zoom = gtk.Button ("Zoom Out")
+        smaller_zoom.connect ("clicked", self.__set_smaller_cb)
+        hbox.pack_start (smaller_zoom)
+        bigger_zoom = gtk.Button ("Zoom In")
+        bigger_zoom.connect ("clicked", self.__set_bigger_cb)
+        hbox.pack_start (bigger_zoom)
+        output_png = gtk.Button ("Output Png")
+        output_png.connect ("clicked", self.__output_png_cb)
+        hbox.pack_start (output_png)
+        window.connect('destroy', gtk.main_quit)
+        window.show_all()
+        #gtk.bindings_activate (gtk.main_quit, 'q', 0)
+        gtk.main()
+    def __set_smaller_cb (self, widget):
+        self.__render.set_smaller_zoom ()
+    def __set_bigger_cb (self, widget):
+        self.__render.set_bigger_zoom ()
+    def __output_png_cb (self, widget):
+        dialog = gtk.FileChooserDialog ("Output Png", self.__window,
+                                        gtk.FILE_CHOOSER_ACTION_SAVE, ("Save",1))
+        self.__dialog = dialog
+        dialog.set_default_response (1)
+        dialog.connect ("response", self.__dialog_response_cb)
+        dialog.show ()
+        return
+    def __dialog_response_cb (self, widget, response):
+        if response == 1:
+            filename = self.__dialog.get_filename ()
+            self.__render.output_png (filename)
+            widget.hide ()
+        return
+
+
+
+def read_data(filename):
+    timelines = Timelines ()
+    colors = Colors ()
+    fh = open(filename)
+    m1 = re.compile ('range ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)')
+    m2 = re.compile ('event-str ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+)')
+    m3 = re.compile ('event-int ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)')
+    m4 = re.compile ('color ([^ ]+) #([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})')
+    for line in fh.readlines():
+        m = m1.match (line)
+        if m:
+            line_name = m.group (1)
+            timeline = timelines.get (m.group (1))
+            rang = timeline.get_range (m.group (2))
+            data_range = DataRange ()
+            data_range.value = m.group (3)
+            data_range.start = int (m.group (4))
+            data_range.end = int (m.group (5))
+            rang.add_range (data_range)
+            continue
+        m = m2.match (line)
+        if m:
+            line_name = m.group (1)
+            timeline = timelines.get (m.group (1))
+            ev = timeline.get_event_str (m.group (2))
+            event = EventString ()
+            event.value = m.group (3)
+            event.at = int (m.group (4))
+            ev.add_event (event)
+            continue
+        m = m3.match (line)
+        if m:
+            line_name = m.group (1)
+            timeline = timelines.get (m.group (1))
+            ev = timeline.get_event_int (m.group (2))
+            event = EventInt ()
+            event.value = int (m.group (3))
+            event.at = int (m.group (4))
+            ev.add_event (event)
+            continue
+        
+        m = m4.match (line)
+        if m:
+            r = int (m.group (2), 16)
+            g = int (m.group (3), 16)
+            b = int (m.group (4), 16)
+            color = Color (r/255, g/255, b/255)
+            colors.add (m.group (1), color)
+            continue
+    timelines.sort ()
+    return (colors, timelines)
+
+
+
+def main():
+    (colors, timelines) = read_data (sys.argv[1])
+    (lower_bound, upper_bound) = timelines.get_bounds ()
+    graphic = GraphicRenderer (lower_bound, upper_bound)
+    top_legend = TopLegendRenderer ()
+    range_values = timelines.get_all_range_values ()
+    range_colors = []
+    for range_value in range_values:
+        range_colors.append (colors.lookup (range_value))
+    top_legend.set_legends (range_values,
+                            range_colors)
+    graphic.set_top_legend (top_legend)
+    data = TimelinesRenderer ()
+    data.set_timelines (timelines, colors)
+    graphic.set_data (data)
+
+    # default range
+    range_mid = (upper_bound - lower_bound) /2
+    range_width = (upper_bound - lower_bound) /10
+    range_lo = range_mid - range_width / 2
+    range_hi = range_mid + range_width / 2
+    graphic.set_range (range_lo, range_hi)
+
+    main_window = MainWindow ()
+    main_window.run (graphic)
+
+
+main ()