00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
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
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
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
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
00138 for area in self._areas:
00139 area._update_y_interval(dc)
00140
00141
00142 max_width = None
00143 for area in self._areas:
00144
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
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
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)
00194 self._legend_margin = ( 6.0, 3.0)
00195 self._legend_line_thickness = 3.0
00196 self._legend_line_width = 9.0
00197 self._legend_font_size = 12.0
00198 self._legend_line_spacing = 1.0
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
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 = []
00225 self._series_data = {}
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
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
00291
00292 palette_offset = property(_get_palette_offset, _set_palette_offset)
00293
00294
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
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
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
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
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
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
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
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
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
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
00626 if tick_x + text_width < self._width - 2:
00627 dc.move_to(tick_x, tick_y)
00628 dc.show_text(s)
00629
00630
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
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
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
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
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)