$search
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)