chart.py
Go to the documentation of this file.
00001 # Software License Agreement (BSD License)
00002 #
00003 # Copyright (c) 2009, Willow Garage, Inc.
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions
00008 # are met:
00009 #
00010 #  * Redistributions of source code must retain the above copyright
00011 #    notice, this list of conditions and the following disclaimer.
00012 #  * Redistributions in binary form must reproduce the above
00013 #    copyright notice, this list of conditions and the following
00014 #    disclaimer in the documentation and/or other materials provided
00015 #    with the distribution.
00016 #  * Neither the name of Willow Garage, Inc. nor the names of its
00017 #    contributors may be used to endorse or promote products derived
00018 #    from this software without specific prior written permission.
00019 #
00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00024 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00031 # POSSIBILITY OF SUCH DAMAGE.
00032 
00033 from __future__ import division
00034 
00035 PKG = 'rxbag_plugins'
00036 import roslib; roslib.load_manifest(PKG)
00037 
00038 import bisect
00039 import math
00040 import sys
00041 import threading
00042 import time
00043 
00044 import cairo
00045 import wx
00046 import wx.lib.wxcairo
00047 
00048 from dataset import DataSet
00049 
00050 class ChartWindow(wx.Window):
00051     def __init__(self, *args, **kwargs):
00052         wx.Window.__init__(self, *args, **kwargs)
00053 
00054         self.background_brush = wx.WHITE_BRUSH
00055 
00056         self._chart = Chart()
00057 
00058         self.Bind(wx.EVT_PAINT, self._on_paint)
00059         self.Bind(wx.EVT_SIZE,  self._on_size)
00060 
00061     def _on_size(self, event):
00062         self._chart.set_size(*self.ClientSize)
00063         self.Refresh()
00064 
00065     def _on_paint(self, event):
00066         window_dc = wx.PaintDC(self)
00067         window_dc.SetBackground(self.background_brush)
00068         window_dc.Clear()
00069 
00070         dc = wx.lib.wxcairo.ContextFromDC(window_dc)
00071         self._chart.paint(dc)
00072 
00073 class Chart(object):
00074     def __init__(self):
00075         self._areas = []
00076 
00077         self._margin_left   = 10
00078         self._margin_right  = 10
00079         self._margin_top    =  8
00080         self._margin_bottom =  2
00081 
00082         self._width  = 100
00083         self._height = 100
00084 
00085     @property
00086     def areas(self): return self._areas
00087 
00088     def set_size(self, size):
00089         self._width, self._height = size
00090 
00091     def create_areas(self, plots):
00092         self._areas = []
00093         palette_offset = 0
00094         for i, plot in enumerate(plots):
00095             area = ChartArea()
00096             area.palette_offset = palette_offset
00097             palette_offset += len(plot)
00098             if i < len(plots) - 1:
00099                 area.show_x_ticks = False
00100             self._areas.append(area)
00101 
00102     def paint(self, dc):
00103         # Clear background to white
00104         dc.set_source_rgb(1, 1, 1)
00105         dc.rectangle(0, 0, self._width, self._height)
00106         dc.fill()
00107 
00108         self._calculate_area_bounds(dc)
00109 
00110         # Paint areas
00111         dc.save()
00112         area_height = self._height / len(self._areas)
00113         for area in self._areas:
00114             area.paint(dc)
00115             dc.translate(0, area_height)
00116         dc.restore()
00117 
00118     def _calculate_area_bounds(self, dc):
00119         if len(self._areas) == 0:
00120             return
00121         
00122         # Calculate top, height
00123         new_bounds_top    = self._margin_top
00124         area_height = self._height / len(self._areas)
00125         new_bounds_height = area_height - new_bounds_top - self._margin_bottom
00126         for area in self._areas:
00127             area._width  = self._width
00128             area._height = area_height
00129 
00130             area._bounds_top = new_bounds_top
00131 
00132             if area.show_x_ticks:
00133                 area._bounds_height = new_bounds_height - 18
00134             else:
00135                 area._bounds_height = new_bounds_height
00136 
00137         # Calculate Y interval
00138         for area in self._areas:
00139             area._update_y_interval(dc)
00140 
00141         # Calculate left, width
00142         max_width = None
00143         for area in self._areas:
00144             # Calculate the maximum width taken up by the Y tick labels
00145             if area.num_points > 0 and area.view_min_y is not None and area.view_max_y is not None and area._y_interval is not None:
00146                 dc.set_font_size(area._tick_font_size)
00147                 for x0, y0, x1, y1 in area._generate_lines_y(area._round_min_to_interval(area.view_min_y, area._y_interval),
00148                                                              area._round_max_to_interval(area.view_max_y, area._y_interval),
00149                                                              area._y_interval,
00150                                                              0, 0):
00151                     s = area.format_y(area.y_chart_to_data(y0))
00152                     text_width = dc.text_extents(s)[2]
00153                     width = text_width + 3
00154     
00155                     if max_width is None or width > max_width:
00156                         max_width = width
00157 
00158         if max_width is not None:
00159             new_bounds_left = self._margin_left + max_width
00160         else:
00161             new_bounds_left = self._margin_left
00162 
00163         new_bounds_width  = self._width - new_bounds_left - self._margin_right
00164 
00165         for area in self._areas:
00166             area._bounds_left  = new_bounds_left
00167             area._bounds_width = new_bounds_width
00168 
00169         # Calculate X interval
00170         for area in self._areas:
00171             area._update_x_interval(dc)
00172 
00173 class ChartArea(object):
00174     def __init__(self):
00175         self._lock = threading.RLock()
00176 
00177         ## Rendering info
00178 
00179         self._width  = 400
00180         self._height = 400
00181 
00182         self._palette = [(0.0, 0.0, 0.7),
00183                          (0.0, 0.7, 0.0),
00184                          (0.7, 0.0, 0.0),
00185                          (0.0, 0.7, 0.7),
00186                          (0.7, 0.0, 0.7),
00187                          (0.7, 0.7, 0.0)]
00188 
00189         self._tick_length        = 4
00190         self._tick_font_size     = 12.0
00191         self._tick_label_padding = 50
00192 
00193         self._legend_position       = ( 7.0, 6.0)   # pixel offset of top-left of legend from top-left of chart
00194         self._legend_margin         = ( 6.0, 3.0)   # internal legend margin  
00195         self._legend_line_thickness =  3.0          # thickness of series color indicator
00196         self._legend_line_width     =  9.0          # width of series color indicator
00197         self._legend_font_size      = 12.0          # height of series font
00198         self._legend_line_spacing   =  1.0          # spacing between lines on the legend
00199 
00200         self._palette_offset = 0
00201         self._show_lines     = True
00202         self._show_points    = True
00203 
00204         self._x_indicator_color     = (1.0, 0.2, 0.2, 0.8)
00205         self._x_indicator_thickness = 2.0
00206         
00207         ###
00208 
00209         self._x_interval   = None
00210         self._y_interval   = None
00211         self._show_x_ticks = True
00212 
00213         # The viewport
00214         self._x_view = None
00215         self._y_view = None
00216         
00217         self.x_indicator = None
00218         self.x_range     = None
00219         
00220         self.data_alpha = 1.0
00221 
00222         ##
00223 
00224         self._series_list = []    # [series0, ...]
00225         self._series_data = {}    # str -> DataSet
00226 
00227         self._bounds_left   = None
00228         self._bounds_top    = None
00229         self._bounds_width  = None
00230         self._bounds_height = None
00231 
00232     @property
00233     def num_points(self):
00234         num_points = 0
00235         for dataset in self._series_data.values():
00236             num_points += dataset.num_points
00237         return num_points
00238 
00239     @property
00240     def min_x(self):
00241         min_x = None
00242         for dataset in self._series_data.values():
00243             if dataset.num_points > 0:
00244                 if min_x is None:
00245                     min_x = dataset.min_x
00246                 else:
00247                     min_x = min(min_x, dataset.min_x)
00248         return min_x
00249 
00250     @property
00251     def max_x(self):
00252         max_x = None
00253         for dataset in self._series_data.values():
00254             if dataset.num_points > 0:
00255                 if max_x is None:
00256                     max_x = dataset.max_x
00257                 else:
00258                     max_x = max(max_x, dataset.max_x)
00259         return max_x
00260 
00261     @property
00262     def min_y(self):
00263         min_y = None
00264         for dataset in self._series_data.values():
00265             if dataset.num_points > 0:
00266                 if min_y is None:
00267                     min_y = dataset.min_y
00268                 else:
00269                     min_y = min(min_y, dataset.min_y)
00270         return min_y
00271 
00272     @property
00273     def max_y(self):
00274         max_y = None
00275         for dataset in self._series_data.values():
00276             if dataset.num_points > 0:
00277                 if max_y is None:
00278                     max_y = dataset.max_y
00279                 else:
00280                     max_y = max(max_y, dataset.max_y)
00281         return max_y
00282 
00283     # palette_offset
00284     
00285     def _get_palette_offset(self):
00286         return self._palette_offset
00287 
00288     def _set_palette_offset(self, palette_offset):
00289         self._palette_offset = palette_offset
00290         # @todo: refresh
00291 
00292     palette_offset = property(_get_palette_offset, _set_palette_offset)
00293 
00294     # show_x_ticks
00295 
00296     def _get_show_x_ticks(self):
00297         return self._show_x_ticks
00298     
00299     def _set_show_x_ticks(self, show_x_ticks):
00300         self._show_x_ticks = show_x_ticks
00301 
00302     show_x_ticks = property(_get_show_x_ticks, _set_show_x_ticks)
00303 
00304     # x_view
00305     
00306     def _get_x_view(self):
00307         if self._x_view is not None:
00308             return self._x_view
00309 
00310         if self.min_x == self.max_x and self.min_x is not None:
00311             return (self.min_x - 0.01, self.max_x + 0.01)
00312         else:
00313             return (self.min_x, self.max_x)
00314 
00315     def _set_x_view(self, x_view):
00316         self._x_view = x_view
00317 
00318     x_view = property(_get_x_view, _set_x_view)
00319 
00320     @property
00321     def y_view(self):
00322         if self._y_view is not None:
00323             return self._y_view
00324 
00325         if self.min_y == self.max_y and self.min_y is not None:
00326             return (self.min_y - 0.01, self.max_y + 0.01)
00327         else:
00328             return (self.min_y, self.max_y)
00329 
00330     @property
00331     def view_range_x(self): return self.view_max_x - self.view_min_x
00332     
00333     @property
00334     def view_min_x(self): return self.x_view[0]
00335 
00336     @property
00337     def view_max_x(self): return self.x_view[1]
00338 
00339     @property
00340     def view_range_y(self): return self.view_max_y - self.view_min_y
00341 
00342     @property
00343     def view_min_y(self): return self.y_view[0]
00344 
00345     @property
00346     def view_max_y(self): return self.y_view[1]
00347 
00348     ## Layout
00349 
00350     @property
00351     def bounds_left(self): return self._bounds_left
00352     
00353     @property
00354     def bounds_top(self): return self._bounds_top
00355 
00356     @property
00357     def bounds_width(self): return self._bounds_width
00358     
00359     @property
00360     def bounds_height(self): return self._bounds_height
00361 
00362     @property
00363     def bounds_right(self): return self._bounds_left + self._bounds_width
00364 
00365     @property
00366     def bounds_bottom(self): return self._bounds_top + self._bounds_height
00367 
00368     ## Data
00369 
00370     def add_datum(self, series, x, y):
00371         with self._lock:
00372             if series not in self._series_data:
00373                 self._series_list.append(series)
00374                 self._series_data[series] = DataSet()
00375 
00376             self._series_data[series].add(x, y)
00377 
00378     def clear(self):
00379         with self._lock:
00380             self._series_list = []
00381             self._series_data = {}
00382 
00383     ## Coordinate transformations
00384 
00385     def coord_data_to_chart(self, x, y): return self.x_data_to_chart(x), self.y_data_to_chart(y)
00386     def x_data_to_chart(self, x):        return self.bounds_left   + (x - self.view_min_x) / self.view_range_x * self.bounds_width
00387     def y_data_to_chart(self, y):        return self.bounds_bottom - (y - self.view_min_y) / self.view_range_y * self.bounds_height
00388     def dx_data_to_chart(self, dx):      return dx / self.view_range_x * self.bounds_width
00389     def dy_data_to_chart(self, dy):      return dy / self.view_range_y * self.bounds_height
00390 
00391     def x_chart_to_data(self, x):        return self.view_min_x + (float(x) - self.bounds_left)   / self.bounds_width  * self.view_range_x
00392     def y_chart_to_data(self, y):        return self.view_min_y + (self.bounds_bottom - float(y)) / self.bounds_height * self.view_range_y
00393     def dx_chart_to_data(self, dx):      return float(dx) / self.bounds_width  * self.view_range_x
00394     def dy_chart_to_data(self, dy):      return float(dy) / self.bounds_height * self.view_range_y
00395     
00396     ## Data formatting
00397     
00398     def format_x(self, x, x_interval=None):
00399         if x_interval is None:
00400             x_interval = self._x_interval
00401         
00402         if self._x_interval is None:
00403             return '%.3f' % x
00404         
00405         dp = max(0, int(math.ceil(-math.log10(x_interval))))
00406         if dp == 0:
00407             return self.format_group(x)
00408         else:
00409             return '%.*f' % (dp, x)
00410     
00411     def format_y(self, y, y_interval=None):
00412         if y_interval is None:
00413             y_interval = self._y_interval
00414         
00415         if y_interval is None:
00416             return '%.3f' % y
00417 
00418         dp = max(0, int(math.ceil(-math.log10(y_interval))))
00419         if dp == 0:
00420             return self.format_group(y)
00421         else:
00422             return '%.*f' % (dp, y)
00423 
00424     def format_group(self, number):
00425         s = '%d' % round(number)
00426         groups = []
00427         while s and s[-1].isdigit():
00428             groups.append(s[-3:])
00429             s = s[:-3]
00430             
00431         return s + ','.join(reversed(groups))
00432 
00433     ## Layout
00434 
00435     def _update_x_interval(self, dc):
00436         if self.num_points == 0:
00437             return
00438 
00439         num_ticks = None
00440         if self.view_min_x is not None and self.view_max_x is not None and self._x_interval is not None:
00441             dc.set_font_size(self._tick_font_size)
00442             for test_num_ticks in range(20, 1, -1):
00443                 test_x_interval = self._get_axis_interval((self.x_view[1] - self.x_view[0]) / test_num_ticks)
00444                 max_width = self._get_max_label_width(dc, test_x_interval)
00445                 
00446                 max_width += 50   # add padding
00447                 
00448                 if max_width * test_num_ticks < self.bounds_width:
00449                     num_ticks = test_num_ticks
00450                     break
00451         
00452         if num_ticks is None:
00453             num_ticks = self.bounds_width / 100
00454 
00455         new_x_interval = self._get_axis_interval((self.x_view[1] - self.x_view[0]) / num_ticks)
00456         if new_x_interval is not None:
00457                 self._x_interval = new_x_interval 
00458 
00459     def _get_max_label_width(self, dc, x_interval):
00460         max_width = None
00461 
00462         for x0, y0, x1, y1 in self._generate_lines_x(self._round_min_to_interval(self.view_min_x, x_interval),
00463                                                      self._round_max_to_interval(self.view_max_x, x_interval),
00464                                                      x_interval,
00465                                                      self.bounds_bottom,
00466                                                      self.bounds_bottom + self._tick_length):
00467             s = self.format_x(self.x_chart_to_data(x0), x_interval)
00468             text_width = dc.text_extents(s)[2]
00469             width = text_width
00470 
00471             if max_width is None or width > max_width:
00472                 max_width = width 
00473 
00474         return max_width
00475 
00476     def _update_y_interval(self, dc):
00477         if self.num_points == 0:
00478             return
00479         
00480         dc.set_font_size(self._tick_font_size)
00481 
00482         label_height = dc.font_extents()[2] + self._tick_label_padding
00483 
00484         num_ticks = self.bounds_height / label_height
00485 
00486         new_y_interval = self._get_axis_interval((self.y_view[1] - self.y_view[0]) / num_ticks)
00487         if new_y_interval is not None:
00488                 self._y_interval = new_y_interval
00489 
00490     def _get_axis_interval(self, range, intervals=[1.0, 2.0, 5.0]):
00491         exp = -8
00492         found = False
00493         prev_threshold = None
00494         while True:
00495             multiplier = pow(10, exp)
00496             for interval in intervals:
00497                 threshold = multiplier * interval
00498                 if threshold > range:
00499                     return prev_threshold
00500                 prev_threshold = threshold
00501             exp += 1
00502 
00503     def _round_min_to_interval(self, min_val, interval, extend_touching=False):
00504         rounded = interval * math.floor(min_val / interval)
00505         if min_val > rounded:
00506             return rounded
00507         if extend_touching:
00508             return min_val - interval
00509         return min_val
00510 
00511     def _round_max_to_interval(self, max_val, interval, extend_touching=False):
00512         rounded = interval * math.ceil(max_val / interval)
00513         if max_val < rounded:
00514             return rounded
00515         if extend_touching:
00516             return max_val + interval
00517         return max_val
00518 
00519     ## Painting
00520 
00521     def paint(self, dc):
00522         self._draw_border(dc)
00523 
00524         dc.save()
00525         dc.rectangle(self.bounds_left, self.bounds_top, self.bounds_width, self.bounds_height)
00526         dc.clip()
00527 
00528         try:
00529             with self._lock:
00530                 self._draw_data_extents(dc)
00531                 self._draw_grid(dc)
00532                 self._draw_axes(dc)
00533                 self._draw_data(dc)
00534                 self._draw_x_indicator(dc)
00535                 self._draw_legend(dc)
00536 
00537         finally:
00538             dc.restore()
00539 
00540         self._draw_ticks(dc)
00541 
00542     def _draw_border(self, dc):
00543         dc.set_antialias(cairo.ANTIALIAS_NONE)
00544         dc.set_line_width(1.0)
00545         dc.set_source_rgba(0, 0, 0, 0.6)
00546         dc.rectangle(self.bounds_left, self.bounds_top - 1, self.bounds_width, self.bounds_height + 1)
00547         dc.stroke()
00548 
00549     def _draw_data_extents(self, dc):
00550         dc.set_source_rgba(0.5, 0.5, 0.5, 0.1)
00551         if self.num_points == 0:
00552                 dc.rectangle(self.bounds_left, self.bounds_top, self.bounds_width, self.bounds_height)
00553         else:
00554                 x_start, x_end = self.x_data_to_chart(self.min_x), self.x_data_to_chart(self.max_x)
00555                 dc.rectangle(self.bounds_left, self.bounds_top, x_start - self.bounds_left,                   self.bounds_height)
00556                 dc.rectangle(x_end,            self.bounds_top, self.bounds_left + self.bounds_width - x_end, self.bounds_height)
00557         dc.fill()
00558 
00559         if self.x_range is not None and self.num_points > 0:
00560             x_range_start, x_range_end = self.x_data_to_chart(self.x_range[0]), self.x_data_to_chart(self.x_range[1])
00561             dc.set_source_rgba(0.2, 0.2, 0.2, 0.1)
00562             dc.rectangle(self.bounds_left, self.bounds_top, x_range_start - self.bounds_left,                   self.bounds_height)
00563             dc.rectangle(x_range_end,      self.bounds_top, self.bounds_left + self.bounds_width - x_range_end, self.bounds_height)
00564             dc.fill()
00565 
00566     def _draw_grid(self, dc):
00567         dc.set_antialias(cairo.ANTIALIAS_NONE)
00568         dc.set_line_width(1.0)
00569         dc.set_dash([2, 4])
00570 
00571         if self.view_min_x != self.view_max_x and self._x_interval is not None:
00572             dc.set_source_rgba(0, 0, 0, 0.4)
00573             x_tick_range = (self._round_min_to_interval(self.view_min_x, self._x_interval),
00574                             self._round_max_to_interval(self.view_max_x, self._x_interval))
00575             self._draw_lines(dc, self._generate_lines_x(x_tick_range[0], x_tick_range[1], self._x_interval))
00576 
00577         if self.view_min_y != self.view_max_y and self._y_interval is not None:
00578             dc.set_source_rgba(0, 0, 0, 0.4)
00579             y_tick_range = (self._round_min_to_interval(self.view_min_y, self._y_interval),
00580                             self._round_max_to_interval(self.view_max_y, self._y_interval))
00581             self._draw_lines(dc, self._generate_lines_y(y_tick_range[0], y_tick_range[1], self._y_interval))
00582 
00583         dc.set_dash([])
00584 
00585     def _draw_axes(self, dc):
00586         dc.set_antialias(cairo.ANTIALIAS_NONE)
00587         dc.set_line_width(1.0)
00588         dc.set_source_rgba(0, 0, 0, 0.3)
00589         
00590         if self.view_min_y != self.view_max_y:
00591             x_intercept = self.y_data_to_chart(0.0)
00592             dc.move_to(self.bounds_left,  x_intercept)
00593             dc.line_to(self.bounds_right, x_intercept)
00594             dc.stroke()
00595         
00596         if self.view_min_x != self.view_max_x:
00597             y_intercept = self.x_data_to_chart(0.0)
00598             dc.move_to(y_intercept, self.bounds_bottom)
00599             dc.line_to(y_intercept, self.bounds_top)
00600             dc.stroke()
00601 
00602     def _draw_ticks(self, dc):
00603         dc.set_antialias(cairo.ANTIALIAS_NONE)
00604         dc.set_line_width(1.0)
00605         
00606         dc.set_font_size(self._tick_font_size)
00607 
00608         # Draw X axis ticks
00609         if self._show_x_ticks and self.view_min_x != self.view_max_x and self._x_interval is not None:
00610             x_tick_range = (self._round_min_to_interval(self.view_min_x, self._x_interval),
00611                             self._round_max_to_interval(self.view_max_x, self._x_interval))
00612             
00613             lines = list(self._generate_lines_x(x_tick_range[0], x_tick_range[1], self._x_interval, self.bounds_bottom, self.bounds_bottom + self._tick_length))
00614 
00615             dc.set_source_rgba(0, 0, 0, 1)
00616             self._draw_lines(dc, lines)
00617 
00618             for x0, y0, x1, y1 in lines:
00619                 s = self.format_x(self.x_chart_to_data(x0))
00620                 text_width, text_height = dc.text_extents(s)[2:4]
00621 
00622                 tick_x = x0 - text_width / 2
00623                 tick_y = y1 + 3 + text_height
00624 
00625                 # Only show label if it's not outside the chart bounds
00626                 if tick_x + text_width < self._width - 2:
00627                     dc.move_to(tick_x, tick_y)
00628                     dc.show_text(s)
00629 
00630         # Draw Y axis ticks
00631         if self.view_min_y != self.view_max_y and self._y_interval is not None:
00632             y_tick_range = (self._round_min_to_interval(self.view_min_y, self._y_interval),
00633                             self._round_max_to_interval(self.view_max_y, self._y_interval))
00634 
00635             lines = list(self._generate_lines_y(y_tick_range[0], y_tick_range[1], self._y_interval, self.bounds_left - self._tick_length, self.bounds_left))
00636 
00637             dc.set_source_rgba(0, 0, 0, 1)
00638             self._draw_lines(dc, lines)
00639 
00640             for x0, y0, x1, y1 in lines:
00641                 s = self.format_y(self.y_chart_to_data(y0))
00642                 text_width, text_height = dc.text_extents(s)[2:4]
00643 
00644                 tick_x = x0 - text_width - 3
00645                 tick_y = y0 + text_height / 2
00646 
00647                 dc.move_to(tick_x, tick_y)
00648                 dc.show_text(s)
00649 
00650     def _draw_lines(self, dc, lines):
00651         for x0, y0, x1, y1 in lines:
00652             dc.move_to(x0, y0)
00653             dc.line_to(x1, y1)
00654         dc.stroke()
00655 
00656     def _generate_lines_x(self, x0, x1, x_step, py0=None, py1=None):
00657         px0 = self.x_data_to_chart(x0)
00658         px1 = self.x_data_to_chart(x1)
00659         if py0 is None:
00660             py0 = self.bounds_bottom
00661         if py1 is None:
00662             py1 = self.bounds_top
00663         px_step = self.dx_data_to_chart(x_step)
00664 
00665         px = px0
00666         while True:
00667             if px >= self.bounds_left and px <= self.bounds_right:
00668                 yield px, py0, px, py1
00669             px += px_step
00670             if px > px1:
00671                 break
00672 
00673     def _generate_lines_y(self, y0, y1, y_step, px0=None, px1=None):
00674         py0 = self.y_data_to_chart(y0)
00675         py1 = self.y_data_to_chart(y1)
00676         if px0 is None:
00677             px0 = self.bounds_left
00678         if px1 is None:
00679             px1 = self.bounds_right
00680         py_step = self.dy_data_to_chart(y_step)
00681         
00682         py = py0
00683         while True:
00684             if py >= self.bounds_top and py <= self.bounds_bottom:
00685                 yield px0, py, px1, py
00686             py -= py_step
00687             if py < py1:
00688                 break
00689 
00690     def _draw_data(self, dc):
00691         if len(self._series_data) == 0:
00692             return
00693 
00694         for series, series_data in self._series_data.items():
00695             dc.set_source_rgba(*self._get_color(series))
00696 
00697             coords = [self.coord_data_to_chart(x, y) for x, y in series_data.points]
00698 
00699             # Only display points with the chart bounds (or 1 off)
00700             filtered_coords = []
00701             x_min, x_max = self.bounds_left, self.bounds_right
00702             for i, (x, y) in enumerate(coords):
00703                 if x < x_min:
00704                     if i < len(coords) - 1 and coords[i + 1][0] >= x_min:
00705                         filtered_coords.append((x, y))
00706                 elif x > x_max:
00707                     if i > 0 and coords[i - 1][0] <= x_max:
00708                         filtered_coords.append((x, y))
00709                 else:
00710                     filtered_coords.append((x, y))
00711             coords = filtered_coords
00712 
00713             if len(coords) == 0:
00714                 continue
00715 
00716             # Draw lines
00717             dc.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
00718             if self._show_lines:
00719                 dc.set_line_width(1.0)
00720                 dc.move_to(*coords[0])
00721                 for px, py in coords:
00722                     dc.line_to(px, py)
00723                 dc.stroke()
00724 
00725             # Draw points
00726             dc.set_antialias(cairo.ANTIALIAS_NONE)
00727             if self._show_points:
00728                 drawn_points = []
00729                 last_x, last_y = -1, -1
00730                 for px, py in coords:
00731                     if abs(px - last_x) >= 3 or abs(py - last_y) >= 3:
00732                         drawn_points.append((px, py))
00733                     last_x, last_y = px, py
00734 
00735                 if len(drawn_points) > 0:
00736                     dc.set_line_width(1.0)
00737                     dc.set_source_rgb(1, 1, 1)
00738                     for px, py in drawn_points:
00739                         dc.rectangle(px - 1, py - 1, 2, 2)
00740                     dc.fill()
00741     
00742                     dc.set_source_rgba(*self._get_color(series))
00743                     for px, py in drawn_points:
00744                         dc.rectangle(px - 1, py - 1, 2, 2)
00745                     dc.stroke()
00746 
00747         dc.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
00748 
00749     def _draw_x_indicator(self, dc):
00750         if self.x_indicator is None or self.view_min_x is None:
00751             return
00752         
00753         dc.set_antialias(cairo.ANTIALIAS_NONE)
00754 
00755         dc.set_line_width(self._x_indicator_thickness)
00756         dc.set_source_rgba(*self._x_indicator_color)
00757 
00758         px = self.x_data_to_chart(self.x_indicator)
00759         
00760         dc.move_to(px, self.bounds_top)
00761         dc.line_to(px, self.bounds_bottom)
00762         dc.stroke()
00763 
00764     def _draw_legend(self, dc):
00765         if len(self._series_list) == 0:
00766                 return
00767         
00768         dc.set_antialias(cairo.ANTIALIAS_NONE)
00769 
00770         dc.set_font_size(self._legend_font_size)
00771         font_height = dc.font_extents()[2]
00772 
00773         dc.save()
00774         
00775         dc.translate(self.bounds_left + self._legend_position[0], self.bounds_top + self._legend_position[1])
00776         
00777         legend_width = 0.0
00778         for series in self._series_list:
00779             legend_width = max(legend_width, self._legend_margin[0] + self._legend_line_width + 3.0 + dc.text_extents(series)[2] + self._legend_margin[0])
00780 
00781         legend_height = self._legend_margin[1] + (font_height * len(self._series_list)) + (self._legend_line_spacing * (len(self._series_list) - 1)) + self._legend_margin[1]
00782 
00783         dc.set_source_rgba(0.95, 0.95, 0.95, 0.5)
00784         dc.rectangle(0, 0, legend_width, legend_height)
00785         dc.fill()
00786         dc.set_source_rgba(0, 0, 0, 0.2)
00787         dc.set_line_width(1.0)
00788         dc.rectangle(0, 0, legend_width, legend_height)
00789         dc.stroke()
00790         
00791         # Drop shadow
00792         dc.set_source_rgba(0.5, 0.5, 0.5, 0.1)
00793         dc.move_to(1, legend_height + 1)
00794         dc.line_to(legend_width + 1, legend_height + 1)
00795         dc.line_to(legend_width + 1, 1)
00796         dc.stroke()
00797 
00798         dc.set_line_width(self._legend_line_thickness)
00799 
00800         dc.translate(self._legend_margin[0], self._legend_margin[1])
00801 
00802         for series in self._series_list:
00803             dc.set_source_rgba(*self._get_color(series, alpha=1.0))
00804 
00805             dc.move_to(0, font_height / 2)
00806             dc.line_to(self._legend_line_width, font_height / 2)
00807             dc.stroke()
00808 
00809             dc.translate(0, font_height)
00810             dc.move_to(self._legend_line_width + 3.0, -3)
00811             dc.show_text(series)
00812 
00813             dc.translate(0, self._legend_line_spacing)
00814 
00815         dc.restore()
00816 
00817     def _get_color(self, series, alpha=None):
00818         index = (self._palette_offset + self._series_list.index(series)) % len(self._palette)
00819         r, g, b = self._palette[index]
00820         if alpha is None:
00821             return (r, g, b, self.data_alpha)
00822         else:
00823             return (r, g, b, alpha)


rxbag_plugins
Author(s): Tim Field
autogenerated on Mon Jan 6 2014 11:54:21