Package qubx :: Module plot
[hide private]
[frames] | no frames]

Source Code for Module qubx.plot

   1  """Cairo-based right-handed 2D graphics with rulers. 
   2   
   3  Copyright 2007-2010 Research Foundation State University of New York  
   4  This file is part of QUB Express.                                           
   5   
   6  QUB Express is free software; you can redistribute it and/or modify           
   7  it under the terms of the GNU General Public License as published by  
   8  the Free Software Foundation, either version 3 of the License, or     
   9  (at your option) any later version.                                   
  10   
  11  QUB Express is distributed in the hope that it will be useful,                
  12  but WITHOUT ANY WARRANTY; without even the implied warranty of        
  13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
  14  GNU General Public License for more details.                          
  15   
  16  You should have received a copy of the GNU General Public License,    
  17  named LICENSE.txt, in the QUB Express program directory.  If not, see         
  18  <http://www.gnu.org/licenses/>.                                       
  19   
  20  """ 
  21   
  22  import pygtk 
  23  pygtk.require('2.0') 
  24  import gtk 
  25  from gtk import gdk 
  26  from gtk import keysyms 
  27  import gobject 
  28   
  29  import gc 
  30  import traceback 
  31   
  32  import collections 
  33  from math import * 
  34  from itertools import * 
  35  from qubx.types import WeakEvent, Reffer 
  36   
  37  try: 
  38      import numpy 
  39      from numpy import * 
  40      have_numpy = True 
  41  except: 
  42      have_numpy = False 
  43   
  44   
45 -def SetupMajorMinor(distance):
46 """ 47 Returns (major, minor) ticks for total axis distance, according to the following:: 48 express distance as x.xxxxxx * 10^y 49 truncate to xEy 50 if x >= 5: 51 major = 5Ey 52 minor = 1Ey 53 else: 54 major = 1Ey 55 minor = 2E(y-1) 56 """ 57 if distance <= 1.0e-10: 58 distance = 1.0 59 expo = floor(log10(distance*1.0/3)) 60 mant = floor(distance/pow(10.0, expo)) 61 if mant >= 5: 62 return (5 * pow(10.0, expo), pow(10.0, expo)) 63 else: 64 return (pow(10.0, expo), 2*pow(10.0, expo-1))
65 66 # regrettably: rule- and user- pix_x refers to the x axis and horizontal user-strip, 67 # but xpct is the portion of unit-width (non-margin) taken by the y axis or vertical user-strip 68 69 INVALID_NONE = 0 70 INVALID_PLOT = 1 71 INVALID_XRULE = 2 72 INVALID_YRULE = 4 73 INVALID_XUSER = 8 74 INVALID_YUSER = 16 75 INVALID_ALL = 255 76
77 -class RulerPlot(gtk.DrawingArea):
78 """ 79 Base class for math-style 2D plots. 80 81 82 @ivar margin: 83 @ivar fgColor: 84 @ivar bgColor: 85 @ivar dataColor: 86 @ivar dataColor2: 87 @ivar pointColor: cross-hairs color 88 @ivar drawPoint: 89 @ivar popup: gtk.Menu on right-click, default None 90 @ivar xAxis: gtk.Adjustment with x bounds and crosshair location 91 @ivar yAxis: gtk.Adjustment with y bounds and crosshair location 92 @ivar OnPlot: L{WeakEvent}(RulerPlot, gtk.Drawable) draw the main plot contents 93 @ivar OnOverlay: L{WeakEvent}(RulerPlot, cr, xPix, yPix) draw overtop the main plot, using cairo context cr in plot coords; 94 xPix, yPix are the dimensions of a pixel in plot coords 95 @ivar OnUserXDraw: L{WeakEvent}(RulerPlot, gtk.Drawable) draw the user-drawn area under the x axis 96 @ivar OnUserYDraw: L{WeakEvent}(RulerPlot, gtk.Drawable) draw the user-drawn area left of the y axis 97 @ivar OnPress: L{WeakEvent}(x, y, e) mouse-down at plot coords x,y; event e 98 @ivar OnRelease: L{WeakEvent}(x, y, e) mouse-up at plot coords x, y; event e 99 @ivar OnDblClick: L{WeakEvent}(x, y, e) double-click at plot coords x,y; event e 100 @ivar OnDrag: L{WeakEvent}(x, y, e) mouse-move when down 101 @ivar OnRoll: L{WeakEvent}(x, y, e) mouse-move when up 102 @ivar OnPopup: L{WeakEvent}(popup) to manipulate menu items before popping 103 """
104 - def __init__(self, margin=10, rule_xpix=20, rule_ypix=20, rule_xformat='%.3g', rule_yformat=lambda x: "%.2f%s" % ScaleUnit(x), 105 user_xpix=0, user_ypix=0, fgColor=(0, 0, 0), bgColor=(1, 1, 1), 106 drawPoint=False, dataColor=(1,0,0), dataColor2=(1,.5,.5)):
107 """ 108 @param margin: number of blank pixels around the edge 109 @param rule_xpix: height of the x-axis ruler in pixels 110 @param rule_ypix: width of the y-axis ruler in pixels 111 @param rule_xformat: format string or function for x axis labels 112 @param rule_yformat: format string or function for y axis labels 113 @param user_xpix: height of the user-drawn area below the x axis 114 @param user_ypix: width of the user-drawn area left of the y axis 115 @param fgColor: (r,g,b) color of axes and labels 116 @param bgColor: (r,g,b) color of background 117 @param drawPoint: if True, draw cross-hairs 118 @param dataColor: (r,g,b) color of primary data points 119 @param dataColor2: (r,g,b) color of secondary data points 120 """ 121 gtk.DrawingArea.__init__(self) 122 self._margin = margin 123 self._rule_xpix = rule_xpix 124 self._rule_ypix = rule_ypix 125 self._rule_xformat = callable(rule_xformat) and rule_xformat or (lambda x: rule_xformat % x) 126 self._rule_yformat = callable(rule_yformat) and rule_yformat or (lambda y: rule_yformat % y) 127 self._user_xpix = user_xpix 128 self._user_ypix = user_ypix 129 self._fgColor = fgColor 130 self._bgColor = bgColor 131 self._drawPoint = drawPoint 132 self._pointColor = [1.0-c for c in bgColor] + [.7] 133 self._dataColor = dataColor 134 # self._setup_dataColor2() #SRB 135 self._dataColor2 = dataColor2 #SRB 136 self._popup = None 137 self._invalid = INVALID_ALL 138 self._map_plot, self._map_last, self._map_xrule, self._map_yrule, self._map_xuser, self._map_yuser = [None]*6 139 self._dim = (0, 0) 140 self._showHint = True 141 self._hint = '' 142 self.xAxis = gtk.Adjustment( value=0, lower=0, upper=1, step_incr=.1, page_incr=1.0, page_size=1.0 ) 143 self.yAxis = gtk.Adjustment( value=0, lower=0, upper=1, step_incr=.1, page_incr=1.0, page_size=1.0 ) 144 self.xAxis.connect("changed", self._onXChanged) 145 self.yAxis.connect("changed", self._onYChanged) 146 self.connect("size_allocate", self.resize_event) 147 self.connect("expose_event", self._expose) 148 self.add_events(gdk.BUTTON_PRESS_MASK | 149 gdk.BUTTON_RELEASE_MASK | 150 gdk.LEAVE_NOTIFY_MASK | 151 gdk.POINTER_MOTION_MASK) 152 self.connect("button_press_event", self.button_press) 153 self.connect("button_release_event", self.button_release) 154 self.connect("motion_notify_event", self.motion_notify) 155 self.connect("leave_notify_event", self.leave_notify) 156 self.OnPlot = WeakEvent() # (RulerPlot, gtk.Drawable) 157 self.OnOverlay = WeakEvent() # (RulerPlot, context (axis coords), xPix, yPix) 158 self.OnUserXDraw = WeakEvent() # (RulerPlot, gtk.Drawable) 159 self.OnUserYDraw = WeakEvent() # (RulerPlot, gtk.Drawable) 160 self.OnPress = WeakEvent() # (x, y, e) 161 self.OnRelease = WeakEvent() # (x, y, e) 162 self.OnDblClick = WeakEvent() # (x, y ,e) 163 self.OnDrag = WeakEvent() # (x, y, e) 164 self.OnRoll = WeakEvent() # (x, y, e) 165 self.OnPopup = WeakEvent() # (popup) to manipulate menu items before popping 166 self._dragging = False 167 self._pixPoint = (0, 0) 168 self._uPoint = (0, 0) 169 self.xPtoC, self.xCtoP, self.yPtoC, self.yCtoP = 4*[lambda x:x] 170 self.fat = 1.0 171 self.unit_width = 0.0 172 self.updateHintCoordsOnRoll = self.__updateHintCoords 173 if drawPoint: 174 self.OnDrag += self.updateHintCoordsOnRoll 175 self.OnRoll += self.updateHintCoordsOnRoll
176 177 # coordinate systems: 178 # p: pixel [0..width] 179 # c: canvas [lower..upper] 180 margin = property(lambda s: s._margin)
181 - def _setBgColor(self, color):
182 self._bgColor = color 183 self._pointColor = [1.0-c for c in self._bgColor] + [.7] 184 # self._setup_dataColor2() #SRB 185 self.redraw_canvas(INVALID_ALL)
186 bgColor = property(lambda s: s._bgColor, _setBgColor)
187 - def _setFgColor(self, color):
188 self._fgColor = color 189 self.redraw_canvas(INVALID_ALL)
190 fgColor = property(lambda s: s._fgColor, _setFgColor) 191
192 - def _setDataColor(self, color):
193 self._dataColor = color 194 # self._setup_dataColor2() #SRB 195 self.redraw_canvas(INVALID_ALL)
196 dataColor = property(lambda s: s._dataColor, _setDataColor) 197 198 #Next def added by SRB
199 - def _setDataColor2(self, color):
200 self._dataColor2 = color 201 self.redraw_canvas(INVALID_ALL)
202 dataColor2 = property(lambda s: s._dataColor2, _setDataColor2) 203
204 - def _setDrawPoint(self, x):
205 self._drawPoint = x 206 if x: 207 self.OnDrag += self.updateHintCoordsOnRoll 208 self.OnRoll += self.updateHintCoordsOnRoll 209 else: 210 self.OnDrag -= self.updateHintCoordsOnRoll 211 self.OnRoll -= self.updateHintCoordsOnRoll 212 self.hint = '' 213 self.redraw_canvas()
214 drawPoint = property(lambda s: s._drawPoint, _setDrawPoint)
215 - def _setPointColor(self, color):
216 self._pointColor = color 217 self.redraw_canvas()
218 pointColor = property(lambda s: s._pointColor, _setPointColor) 219 220 #SRB 221 # def _setup_dataColor2(self): 222 # self._dataColor2 = [(x+y)/2.0 for x,y in zip(self._bgColor, self._dataColor)] 223
224 - def _setPopup(self, x):
225 self._popup = x
226 popup = property(lambda s: s._popup, _setPopup)
227 - def _setShowHint(self, x):
228 self._showHint = x 229 self.redraw_canvas()
230 showHint = property(lambda s: s._showHint, _setShowHint)
231 - def _setHint(self, x):
232 self._hint = x 233 if self._showHint: 234 self.redraw_canvas()
235 hint = property(lambda s: s._hint, _setHint) 236
237 - def redraw_canvas(self, invalid=INVALID_NONE, immediately=False):
238 """ 239 Requests various levels of repaint, or does it immediately. 240 invalid can be any combination of 241 - INVALID_NONE -- just redraw the overlay 242 - INVALID_PLOT -- redraw the main plot and the overlay 243 - INVALID_XRULE -- redraw the x axis ruler 244 - INVALID_YRULE -- redraw the y axis ruler 245 - INVALID_XUSER -- redraw the x axis user-draw area 246 - INVALID_YUSER -- redraw the y axis user-draw area 247 - INVALID_ALL = INVALID_PLOT | INVALID_XRULE | INVALID_YRULE | ... 248 """ 249 self._invalid |= invalid 250 if self.window: 251 alloc = self.get_allocation() 252 rect = gdk.Rectangle(0, 0, alloc.width, alloc.height) 253 self.window.invalidate_rect(rect, True) 254 if immediately: 255 self.window.process_updates(True)
256 - def _resetPoint(self):
257 self._uPoint = (self.xPtoC(self._pixPoint[0]), self.yPtoC(self._pixPoint[1])) 258 self.xAxis.set_value(self._uPoint[0]) 259 self.yAxis.set_value(self._uPoint[1])
260 - def _setupAxes(self):
261 w, h = self._dim 262 w += 2*self._margin 263 h += 2*self._margin 264 plot_xpix = max(1, w - 2*self._margin - self._rule_ypix - self._user_ypix) 265 plot_ypix = max(1, h - 2*self._margin - self._rule_xpix - self._user_xpix) 266 xx = self.xAxis 267 yy = self.yAxis 268 dx = xx.upper - xx.lower 269 dy = yy.upper - yy.lower 270 if dx <= 1.0e-8: dx = 1.0 271 if dy <= 1.0e-8: dy = 1.0 272 px0 = self._margin + self._rule_ypix + self._user_ypix 273 py0 = self._margin 274 self.xPtoC = lambda x: xx.lower + (dx/plot_xpix) * (x - px0) 275 self.xCtoP = lambda x: px0 + (plot_xpix/dx) * (x - xx.lower) 276 self.yPtoC = lambda y: yy.upper - (dy/plot_ypix) * (y - py0) 277 self.yCtoP = lambda y: py0 + (plot_ypix/dy) * (yy.upper - y) 278 self._resetPoint()
279 - def resize_event(self, widget, rect):
281 - def _onXChanged(self, xAxis):
283 - def _onYChanged(self, yAxis):
285 286 #SRB: Patched to fix double click problem (i.e., double click is only a press event, not 287 #SRB: a release event) and to allow pass through of button presses on all 3 buttons. 288 #SRB: The release event was otherwise left alone so as to not break the protocol editor. 289
290 - def button_press(self, widget, event):
291 if self._popup and event.button == 3: 292 pass 293 elif event.type == gdk._2BUTTON_PRESS: 294 self.OnDblClick(self.xAxis.value, self.yAxis.value, event) 295 else: 296 self._dragging = (event.button==1) #SRB 297 self.OnPress(self.xAxis.value, self.yAxis.value, event) #SRB 298 # if event.button == 1: 299 # self._dragging = True 300 # self.OnPress(self.xAxis.value, self.yAxis.value, event) 301 return False
302 - def button_release(self, widget, event):
303 if self._popup and event.button == 3: 304 self.OnPopup(self._popup) 305 self._popup.popup(None, None, None, event.button, event.time) 306 elif self._dragging: 307 self._dragging = False 308 self.OnRelease(self.xAxis.value, self.yAxis.value, event) 309 return False
310 - def motion_notify(self, widget, event):
311 # event.x, event.y 312 self._pixPoint = (event.x, event.y) 313 self._resetPoint() 314 self.redraw_canvas() 315 if self._dragging: 316 self.OnDrag(self.xAxis.value, self.yAxis.value, event) 317 else: 318 self.OnRoll(self.xAxis.value, self.yAxis.value, event)
319 - def leave_notify(self, widget, event):
320 self._uPoint = (self.xAxis.lower-1, self.yAxis.lower-1) 321 if self._drawPoint or (self._hint and self._showHint): 322 self.redraw_canvas()
323 - def __updateHintCoords(self, x, y, e):
324 self.hint = '(%s, %s)' % (self._rule_xformat(x), self._rule_yformat(y))
325 - def transform(self, context, x0, y0, x1, y1, xx0, yy0, xx1, yy1, xPix, yPix):
326 """ 327 Translates and scales the cairo context so the rectangle between x0,y0 and x1,y1 328 is addressed from xx0,yy0 to xx1,yy1. xPix and yPix are the input dimensions of one pixel. 329 Returns the new (xPix, yPix). 330 331 Typical usage, to change screen coords into abstract coords: 332 333 >>> xPix, yPix = plot.transform(cr, 0, 0, w, h, x0, y0, x1, y1, 1.0, 1.0) # left-handed / large y at bottom 334 >>> xPix, yPix = plot.transform(cr, 0, 0, w, h, x0, y1, x1, y0, 1.0, 1.0) # right-handed / large y at top 335 """ 336 xScale = (x1-x0)*1.0/(xx1-xx0) 337 yScale = (y1-y0)*1.0/(yy1-yy0) 338 context.translate(x0, y0) 339 context.scale(xScale, yScale) 340 context.translate(-xx0, -yy0) 341 return (abs(xPix/xScale), abs(yPix/yScale))
342 - def _expose(self, widget, event):
343 self.draw_to_drawable(self.window)
344 - def draw_to_drawable(self, drawable, px0=0, py0=0, width=0, height=0):
345 """Draws everything to drawable with origin px0,py0, resizing if you specify width and height.""" 346 dw, dh = drawable.get_size() 347 w = width or dw 348 h = height or dh 349 m = self._margin 350 w -= 2*m 351 h -= 2*m 352 if (w<1) or (h<1): return 353 354 plot_xpix = max(1, w - self._rule_ypix - self._user_ypix) 355 plot_ypix = max(1, h - self._rule_xpix - self._user_xpix) 356 x0, y0 = self.xAxis.lower, self.yAxis.lower 357 if self.xAxis.upper > x0: x1 = self.xAxis.upper 358 else: x1 = x0 + 1 359 if self.yAxis.upper > y0: y1 = self.yAxis.upper 360 else: y1 = y0 + 1 361 if (not self._map_plot) or ((w, h) != self._dim): 362 self._dim = (w, h) 363 self._invalid = INVALID_ALL 364 self._map_plot = gdk.Pixmap(self.window, plot_xpix, plot_ypix) 365 self._map_last = gdk.Pixmap(self.window, plot_xpix, plot_ypix) 366 self._map_xrule = self._rule_xpix and gdk.Pixmap(self.window, plot_xpix, self._rule_xpix) 367 self._map_yrule = self._rule_ypix and gdk.Pixmap(self.window, self._rule_ypix, plot_ypix) 368 self._map_xuser = self._user_xpix and gdk.Pixmap(self.window, plot_xpix, self._user_xpix) 369 self._map_yuser = self._user_ypix and gdk.Pixmap(self.window, self._user_ypix, plot_ypix) 370 gc.collect() 371 if self._invalid: 372 self._setupAxes() 373 if self._user_xpix and self._invalid & INVALID_XUSER: 374 subw, subh = self._map_xuser.get_size() 375 subcontext = self._map_xuser.cairo_create() 376 subcontext.set_source_rgb(*self.bgColor) 377 subcontext.paint() 378 self.OnUserXDraw(self, subcontext, subw, subh, 1.0, 1.0) 379 if self._user_ypix and self._invalid & INVALID_YUSER: 380 subw, subh = self._map_yuser.get_size() 381 subcontext = self._map_yuser.cairo_create() 382 subcontext.set_source_rgb(*self.bgColor) 383 subcontext.paint() 384 self.OnUserYDraw(self, subcontext, subw, subh, 1.0, 1.0) 385 if self._rule_xpix and self._invalid & INVALID_XRULE: 386 subw, subh = self._map_xrule.get_size() 387 subcontext = self._map_xrule.cairo_create() 388 subcontext.set_source_rgb(*self.bgColor) 389 subcontext.paint() 390 self._drawXrule(subcontext, x0, x1, subw, subh, 1.0, 1.0) 391 if self._rule_ypix and self._invalid & INVALID_YRULE: 392 subw, subh = self._map_yrule.get_size() 393 subcontext = self._map_yrule.cairo_create() 394 subcontext.set_source_rgb(*self.bgColor) 395 subcontext.paint() 396 self._drawYrule(subcontext, y0, y1, subw, subh, 1.0, 1.0) 397 if self._invalid & INVALID_PLOT: 398 self._map_last, self._map_plot = self._map_plot, self._map_last 399 subw, subh = self._map_plot.get_size() 400 subcontext = self._map_plot.cairo_create() 401 subcontext.set_source_rgb(*self.bgColor) 402 subcontext.paint() 403 self.OnPlot(self, subcontext, self._invalid & (INVALID_XRULE|INVALID_YRULE), 404 subw, subh, 1.0, 1.0) 405 self._invalid = INVALID_NONE 406 407 context = drawable.cairo_create() 408 context.set_source_rgb(*self._bgColor) 409 context.rectangle(0, 0, w+2*m, h+2*m) 410 context.fill() 411 del context 412 grc = drawable.new_gc() 413 drawable.draw_drawable(grc, self._map_plot, 0, 0, w+m-plot_xpix, m, -1, -1) 414 if self._rule_xpix: drawable.draw_drawable(grc, self._map_xrule, 0, 0, px0+w+m-plot_xpix, py0+m+plot_ypix, -1, -1) 415 if self._rule_ypix: drawable.draw_drawable(grc, self._map_yrule, 0, 0, px0+m+self._user_ypix, py0+m, -1, -1) 416 if self._user_xpix: drawable.draw_drawable(grc, self._map_xuser, 0, 0, px0+w+m-plot_xpix, py0+h+m-self._user_xpix, -1, -1) 417 if self._user_ypix: drawable.draw_drawable(grc, self._map_yuser, 0, 0, px0+m, py0+m, -1, -1) 418 del grc 419 420 context = drawable.cairo_create() 421 context.rectangle(px0+self._margin, py0+self._margin, w, h) 422 context.clip() 423 context.set_source_rgba(*self._pointColor) 424 context.translate(px0+self._margin, py0+self._margin) 425 426 context.save() 427 xPix, yPix = self.transform(context, w-plot_xpix, 0, w, plot_ypix, 428 x0, y1, x1, y0, 1, 1) 429 self.OnOverlay(self, context, xPix, yPix) 430 context.restore() 431 432 drawPoint = (self.drawPoint 433 and (x0 < self._uPoint[0] < x1) 434 and (y0 < self._uPoint[1] < y1)) 435 if drawPoint: 436 if self._rule_xpix: self._overlayXrule(context, w, h, x0, x1) 437 if self._rule_ypix: self._overlayYrule(context, w, h, y0, y1) 438 xPix, yPix = self.transform(context, w-plot_xpix, 0, w, plot_ypix, 439 x0, y1, x1, y0, 1, 1) 440 if drawPoint: 441 self.set_line_width(context, xPix, 1) 442 context.move_to(self._uPoint[0], y1) 443 context.line_to(self._uPoint[0], y0) 444 context.stroke() 445 self.set_line_width(context, yPix, 1) 446 context.move_to(x1, self._uPoint[1]) 447 context.line_to(x0, self._uPoint[1]) 448 context.stroke() 449 450 if (self._showHint and self._hint 451 and (x0 < self._uPoint[0] < x1) 452 and (y0 < self._uPoint[1] < y1)): 453 context.save() 454 context.translate(self._uPoint[0], self._uPoint[1]) 455 context.scale(xPix, yPix) 456 context.scale(1.0, -1.0) # un-flip 457 context.translate(12+4, 4) 458 context.set_font_size(10) 459 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 460 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(self._hint) 461 context.set_source_rgba(.9, 1, 0, .75) 462 context.rectangle(-4, -4, width+8, height+8) 463 context.fill() 464 context.set_source_rgba(0, 0, 0, .75) 465 context.move_to(0, -ybearing) 466 context.show_text(self._hint) 467 context.restore()
468 469
470 - def draw_to_context(self, context, width, height, fat=1.0, unit_width=0.0, reallyDrawPoint=False):
471 """ 472 Draws everything to a cairo context with width and height. 473 474 @param fat: line width scaler 475 @param unit_width: kluge for printing 476 @param reallyDrawPoint: False to suppress cross-hairs even when drawPoint==True 477 """ 478 f, u = self.fat, self.unit_width 479 self.fat, self.unit_width = fat, unit_width 480 w, h = width, height 481 m = self._margin 482 w -= 2*m 483 h -= 2*m 484 if (w<1) or (h<1): return 485 px0, py0 = 0, 0 486 487 plot_xpix = max(1, w - self._rule_ypix - self._user_ypix) 488 plot_ypix = max(1, h - self._rule_xpix - self._user_xpix) 489 x0, y0 = self.xAxis.lower, self.yAxis.lower 490 if self.xAxis.upper > x0: x1 = self.xAxis.upper 491 else: x1 = x0 + 1 492 if self.yAxis.upper > y0: y1 = self.yAxis.upper 493 else: y1 = y0 + 1 494 495 if self._user_xpix: 496 context.save() 497 xPix, yPix = self.transform(context, px0+w+m-plot_xpix, py0+h+m-self._user_xpix, px0+w+m, py0+h+m, 0, 0, plot_xpix, self._user_xpix, 1.0, 1.0) 498 context.rectangle(0, 0, plot_xpix, self._user_xpix) 499 context.clip() 500 context.set_source_rgb(1,1,1) 501 context.paint() 502 self.OnUserXDraw(self, context, plot_xpix, self._user_xpix, xPix, yPix) 503 context.restore() 504 505 if self._user_ypix: 506 context.save() 507 xPix, yPix = self.transform(context, px0+m, py0+m, px0+m+self._user_ypix, py0+m+plot_ypix, 0, 0, self._user_ypix, plot_ypix, 1.0, 1.0) 508 context.rectangle(0, 0, self._user_ypix, plot_ypix) 509 context.clip() 510 context.set_source_rgb(1,1,1) 511 context.paint() 512 self.OnUserYDraw(self, context, self._user_ypix, plot_ypix, xPix, yPix) 513 context.restore() 514 515 if self._rule_xpix: 516 context.save() 517 xPix, yPix = self.transform(context, px0+w+m-plot_xpix, py0+m+plot_ypix, px0+w+m, py0+m+plot_ypix+self._rule_xpix, 0, 0, plot_xpix, self._rule_xpix, 1.0, 1.0) 518 context.rectangle(0, 0, plot_xpix, self._rule_xpix) 519 context.clip() 520 context.set_source_rgb(1,1,1) 521 context.paint() 522 self._drawXrule(context, x0, x1, plot_xpix, self._rule_xpix, xPix, yPix) 523 context.restore() 524 525 if self._rule_ypix: 526 context.save() 527 xPix, yPix = self.transform(context, px0+m+self._user_ypix, py0+m, px0+m+self._user_ypix+self._rule_ypix, py0+m+plot_ypix, 0, 0, self._rule_ypix, plot_ypix, 1.0, 1.0) 528 context.rectangle(0, 0, self._rule_ypix, plot_ypix) 529 context.clip() 530 context.set_source_rgb(1,1,1) 531 context.paint() 532 self._drawYrule(context, y0, y1, self._rule_ypix, plot_ypix, xPix, yPix) 533 context.restore() 534 535 context.save() 536 537 xPix, yPix = self.transform(context, px0+w+m-plot_xpix, py0+m, px0+w+m, py0+m+plot_ypix, 0, 0, plot_xpix, plot_ypix, 1.0, 1.0) 538 context.rectangle(0, 0, plot_xpix, plot_ypix) 539 context.clip() 540 context.set_source_rgb(1,1,1) 541 context.paint() 542 543 context.save() 544 self.OnPlot(self, context, True, plot_xpix, plot_ypix, xPix, yPix) 545 context.restore() 546 547 context.save() 548 xPix, yPix = self.transform(context, 0, 0, plot_xpix, plot_ypix, x0, y1, x1, y0, xPix, yPix) 549 self.OnOverlay(self, context, xPix, yPix) 550 context.restore() 551 552 drawPoint = (self.drawPoint and reallyDrawPoint 553 and (x0 < self._uPoint[0] < x1) 554 and (y0 < self._uPoint[1] < y1)) 555 if drawPoint: 556 if self._rule_xpix: self._overlayXrule(context, w, h, x0, x1) 557 if self._rule_ypix: self._overlayYrule(context, w, h, y0, y1) 558 xPix, yPix = self.transform(context, 0, 0, plot_xpix, plot_ypix, x0, y1, x1, y0, xPix, yPix) 559 if drawPoint: 560 context.set_source_rgba(*self._pointColor) 561 self.set_line_width(context, xPix, 1) 562 context.move_to(self._uPoint[0], y1) 563 context.line_to(self._uPoint[0], y0) 564 context.stroke() 565 self.set_line_width(context, yPix, 1) 566 context.move_to(x1, self._uPoint[1]) 567 context.line_to(x0, self._uPoint[1]) 568 context.stroke() 569 570 if (self._showHint and self._hint and reallyDrawPoint 571 and (x0 < self._uPoint[0] < x1) 572 and (y0 < self._uPoint[1] < y1)): 573 context.save() 574 context.translate(self._uPoint[0], self._uPoint[1]) 575 context.scale(xPix, yPix) 576 context.scale(1.0, -1.0) # un-flip 577 context.translate(12+4, 4) 578 context.set_font_size(10) 579 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() 580 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(self._hint) 581 context.set_source_rgba(.9, 1, 0, .75) 582 context.rectangle(-4, -4, width+8, height+8) 583 context.fill() 584 context.set_source_rgba(0, 0, 0, .75) 585 context.move_to(0, -ybearing) 586 context.show_text(self._hint) 587 context.restore() 588 589 context.restore() 590 # restore non-printing line thickness 591 self.fat, self.unit_width = f, u
592 - def cairo_create(self, drawable, x0, y0, x1, y1, blank=True):
593 """ 594 Returns a cairo context with blanked background and right-handed coordinate system. 595 @return: context, xPix, yPix, x0, y0, x1, y1. 596 """ 597 context = drawable.cairo_create() 598 if blank: 599 context.set_source_rgb(*self.bgColor) 600 context.paint() 601 w, h = drawable.get_size() 602 if x1 <= x0: x1 = x0 + 1 603 if y1 <= y0: y1 = y0 + 1 604 xPix, yPix = self.transform(context, 0, 0, w, h, x0, y1, x1, y0, 1, 1) 605 return context, xPix, yPix, x0, y0, x1, y1
606 - def set_line_width(self, context, perPix, points):
607 """Part of the printing line-width kluge.""" 608 if self.unit_width: 609 context.set_line_width(points*self.fat*self.unit_width)# 2*yPix) 610 else: 611 context.set_line_width(points*self.fat*perPix)
612 - def _drawXrule(self, context, x0, x1, w, h, xPix, yPix):
613 xPix, yPix = self.transform(context, 0, 0, w, h, x0, 1, x1, 0, xPix, yPix) 614 self._drawHrule(context, minor=(0.5, 1.0), major=(0.0, 1.0), topline=1.0, 615 baseline=0.0, fontHeight=0.6, 616 a=x0, b=x1, xPix=xPix, yPix=yPix, format=self._rule_xformat)
617 - def _drawYrule(self, context, y0, y1, w, h, xPix, yPix):
618 xPix, yPix = self.transform(context, 0, 0, w, h, 0, y1, 1, y0, xPix, yPix) 619 context.translate(1, self.yAxis.lower) 620 context.rotate(pi/2) 621 context.translate(-self.yAxis.lower, 0) 622 self._drawHrule(context, minor=(0.0, 0.3), major=(0.0, 0.6), topline=0.0, 623 baseline=0.4, fontHeight=0.6, 624 a=y0, b=y1, xPix=yPix, yPix=xPix, format=self._rule_yformat)
625 - def _drawHrule(self, context, minor, major, topline, baseline, fontHeight, a, b, xPix, yPix, format):
626 context.save() 627 context.set_source_rgb(*self.bgColor) 628 context.rectangle(a, 0.0, b-a, 1.0) 629 context.fill() 630 context.set_source_rgb(*self.fgColor) 631 self.set_line_width(context, yPix, 1) 632 context.move_to(a, topline) 633 context.line_to(b, topline) 634 context.stroke() 635 def drawLines(dist, ends): 636 x = dist * ceil(a / dist) 637 while x < b: 638 context.move_to(x, ends[0]) 639 context.line_to(x, ends[1]) 640 context.stroke() 641 x += dist
642 maj_dist, min_dist = SetupMajorMinor(b-a) 643 while (maj_dist/xPix) < 50: 644 maj_dist, min_dist = maj_dist*2, min_dist*2 645 while (maj_dist/xPix) > 300: 646 maj_dist, min_dist = maj_dist/2, min_dist/2 647 self.set_line_width(context, xPix, 1) 648 drawLines(maj_dist, major) 649 drawLines(min_dist, minor) 650 fontsize = max(7, int(fontHeight * self.fat / yPix)) 651 context.set_font_size(fontsize) 652 lastLblEnd = [-1e10] 653 def drawLbl(x, lbl): 654 # transform x,baseline to 0,0 with actual-pixel scaling: 655 context.save() 656 context.translate(x, baseline) 657 context.scale(xPix, yPix) 658 context.scale(1.0, -1.0) # un-flip 659 context.move_to(0, 0) 660 x_bearing, y_bearing, width, height, x_advance, y_advance = context.text_extents(lbl) 661 context.show_text(lbl) 662 context.restore() 663 lastLblEnd[0] = x + x_advance*xPix
664 x = maj_dist * ceil(a / maj_dist) 665 if x == - 0.0: 666 x = 0.0 667 while x < b: 668 if x >= lastLblEnd[0]: 669 drawLbl(x, format(x)) 670 x += maj_dist 671 context.restore()
672 - def _overlayXrule(self, context, w, h, x0, x1):
673 context.save() 674 xPix, yPix = self.transform(context, self._user_ypix+self._rule_ypix, h-self._user_xpix-self._rule_xpix, 675 w, h-self._user_xpix, 676 x0, 1, x1, 0, 1, 1) 677 self._overlayHrule(context, topline=1.0, baseline=0.0, fontHeight=0.6, 678 a=x0, b=x1, xPix=xPix, yPix=yPix, format=self._rule_xformat, point=self._uPoint[0]) 679 context.restore()
680 - def _overlayYrule(self, context, w, h, y0, y1):
681 context.save() 682 xPix, yPix = self.transform(context, self._user_ypix, 0, 683 self._user_ypix+self._rule_ypix, h-self._user_xpix-self._rule_xpix, 684 0, y1, 1, y0, 1, 1) 685 context.translate(1, self.yAxis.lower) 686 context.rotate(pi/2) 687 context.translate(-self.yAxis.lower, 0) 688 self._overlayHrule(context, topline=0.0, baseline=0.4, fontHeight=0.6, 689 a=self.yAxis.lower, b=self.yAxis.upper, xPix=yPix, yPix=xPix, format=self._rule_yformat, 690 point=self._uPoint[1]) 691 context.restore()
692 - def _overlayHrule(self, context, topline, baseline, fontHeight, a, b, xPix, yPix, format, point):
693 fontsize = max(7, int(fontHeight * self.fat / yPix)) 694 context.set_font_size(fontsize) 695 lastLblEnd = [-1e10] # boxed to be modified by drawLbl() 696 def drawLbl(x, lbl): 697 # transform x,baseline to 0,0 with actual-pixel scaling: 698 context.save() 699 context.translate(x, baseline) 700 context.scale(xPix, yPix) 701 context.scale(1.0, -1.0) # un-flip 702 context.move_to(0, 0) 703 x_bearing, y_bearing, width, height, x_advance, y_advance = context.text_extents(lbl) 704 context.show_text(lbl) 705 context.restore() 706 lastLblEnd[0] = x + x_advance*xPix
707 s = format(point) 708 context.set_source_rgb(*self.bgColor) 709 drawLbl(point, s) 710 context.rectangle(point, baseline, lastLblEnd[0]-point, fontHeight) 711 context.fill() 712 context.set_source_rgba(*self.pointColor) 713 self.set_line_width(context, xPix, 1) 714 context.move_to(point, 0.0) 715 context.line_to(point, 1.0) 716 context.stroke() 717 drawLbl(point, s) 718 719
720 -class ScrollPlot(RulerPlot):
721 """ 722 A specialization of RulerPlot for data that arrives point-by-point, 723 scrolling in the positive x direction. 724 725 @ivar duration: 726 @ivar yBounds: 727 """
728 - def __init__(self, duration=10.0, yBounds=(0.0, 1.0), **kw):
729 """ 730 @param duration: keep the x axis between (x_last - duration, x_last) 731 @param yBounds: (y_low, y_high) 732 """ 733 if not kw.has_key("drawPoint"): kw["drawPoint"] = True 734 RulerPlot.__init__(self, **kw) 735 self.set_size_request(100, 70) 736 self.past = collections.deque() 737 self.seen = collections.deque() 738 self.fresh = collections.deque() 739 self._setDuration(duration) 740 self._setYBounds(yBounds) 741 self._lastdim = None 742 self._xlast = 0 743 self._xfirst = 0 744 self._tscroll = 0 745 self._xr = 0 746 self.r = Reffer() 747 self.OnPlot += self.r(self._onPlot)
748 - def _setYBounds(self, yBounds):
749 self._yBounds = yBounds 750 if yBounds: 751 self.yAxis.lower = yBounds[0] 752 self.yAxis.upper = yBounds[1] 753 self.yAxis.emit('changed')
754 yBounds = property(lambda s: s._yBounds, _setYBounds)
755 - def _setDuration(self, x):
756 self._duration = x 757 self.xAxis.lower = -x 758 self.xAxis.upper = 0.0 759 self.xAxis.emit('changed')
760 duration = property(lambda s: s._duration, _setDuration)
761 - def add(self, *dat):
762 """ 763 add(x, y, y2, ...) adds a datapoint with one or more y coordinates. 764 Points beyond duration are removed onPlot.""" 765 x = dat[0] 766 if x < self._xlast: 767 self.past.clear(); self.seen.clear(); self.fresh.clear() 768 self._lastmap = None # force re-plot 769 self.fresh.append(dat) 770 self._xlast = x 771 self.redraw_canvas(INVALID_PLOT)
772 - def get_data(self):
773 """Returns a list of all visible datapoints.""" 774 data = [] 775 for deq in (self.past, self.seen, self.fresh): 776 for x in deq: 777 data.append(x) 778 return data
779 - def _onPlot(self, plot, context, fullyInvalid, w, h, xPix, yPix):
780 effw = w 781 if 0 == (len(self.past) + len(self.seen) + len(self.fresh)): 782 context.set_source_rgb(*self.bgColor) 783 context.paint() 784 return 785 d = float(self._duration) 786 last_xr = self._xr 787 last_x0 = last_xr - self._duration 788 lxToP = lambda x: (x - last_x0)*effw/d 789 lxToPq = lambda x: int(round(lxToP(x))) 790 if len(self.fresh) and (lxToPq(self.fresh[-1][0]) > lxToPq(last_xr)): 791 scroll = lxToPq(self.fresh[-1][0]) - lxToPq(last_xr) 792 self._tscroll += scroll 793 self._xr = last_xr + scroll*d/effw 794 else: 795 scroll = 0 796 xr = self._xr 797 x0 = xr - d 798 xToP = lambda x: (x - x0) * effw/d 799 xToPq = lambda x: int(round(xToP(x))) 800 y0 = self.yAxis.lower 801 dy = self.yAxis.upper - y0 802 if dy <= 0: 803 dy = 1.0 804 y1 = y0 + dy 805 yToP = lambda y: h - (y - y0)*h/dy 806 yToPq = lambda y: int(round(yToP(y))) 807 xrpl = w - scroll 808 while len(self.seen) and (xToPq(self.seen[0][0]) < xrpl): 809 self.past.append(self.seen.popleft()) 810 while len(self.past) and (self.past[0][0] < x0): 811 self.past.popleft() 812 def Plot(pts): 813 context.set_source_rgb(*self.dataColor) 814 for p in pts: 815 x, y = xToPq(p[0]), yToPq(p[1]) 816 context.rectangle(x-1, y-1, 2, 2) 817 context.fill() 818 if len(p) > 2: 819 context.set_source_rgb(*self._dataColor2) 820 for z in p[2:]: 821 context.rectangle(x-1, yToPq(z)-1, 2, 2) 822 context.fill() 823 context.set_source_rgb(*self.dataColor)
824 if fullyInvalid or ((w, h) != self._lastdim): 825 self._lastdim = (w, h) 826 for c in [self._map_last.cairo_create(), context]: 827 c.set_source_rgb(*self.bgColor) 828 c.paint() 829 Plot(self.past) 830 Plot(self.seen) 831 else: 832 context.set_source_pixmap(self._map_last, -scroll, 0) 833 context.rectangle(0, 0, w-scroll, h) 834 context.fill() 835 context.set_source_rgb(*self.bgColor) 836 context.rectangle(w-scroll, 0, w, h) 837 context.fill() 838 if scroll: 839 Plot(self.seen) 840 Plot(self.fresh) 841 while len(self.fresh): 842 pt = self.fresh.popleft() 843 if xToPq(pt[0]) < (w-1): 844 self.past.append(pt) 845 else: 846 self.seen.append(pt)
847
848 -class SampledScrollPlot(RulerPlot):
849 """ 850 A specialization of RulerPlot for evenly sampled data that arrives one or more point at a time, 851 scrolling in the positive x direction. 852 853 @ivar duration: 854 @ivar yBounds: 855 @ivar sampling: 856 """
857 - def __init__(self, duration=10.0, yBounds=(0.0, 1.0), sampling=1e-3, **kw):
858 """ 859 @param duration: keep the x axis between (x_last - duration, x_last) 860 @param yBounds: (y_low, y_high) 861 @param sampling: interval between datapoints, in seconds 862 """ 863 if not kw.has_key("drawPoint"): kw["drawPoint"] = True 864 RulerPlot.__init__(self, **kw) 865 self.set_size_request(100, 70) 866 self.past = collections.deque() 867 self.seen = collections.deque() 868 self.fresh = collections.deque() 869 self._sampling = sampling 870 self._setDuration(duration) 871 self._setYBounds(yBounds) 872 self._lastdim = None 873 self._xlast = 0 874 self._xfirst = 0 875 self._tscroll = 0 876 self._xr = 0 877 self._now = 0.0 878 self._drawn = 1e10 879 self.r = Reffer() 880 self.OnPlot += self.r(self._onPlot)
881 - def _setYBounds(self, yBounds):
882 self._yBounds = yBounds 883 if yBounds: 884 self.yAxis.lower = yBounds[0] 885 self.yAxis.upper = yBounds[1] 886 self.yAxis.emit('changed')
887 yBounds = property(lambda s: s._yBounds, _setYBounds)
888 - def _setDuration(self, x):
889 self._target_dur = x 890 self.xAxis.lower = -x 891 self.xAxis.upper = 0.0 892 self.xAxis.emit('changed')
893 duration = property(lambda s: s._target_dur, _setDuration)
894 - def _setSampling(self, x):
895 self._sampling = x 896 self._setDuration(self._target_dur)
897 sampling = property(lambda s: s._sampling, _setSampling)
898 - def add(self, arr):
899 """ 900 add(yy) appends the y values from a numpy array. FUTURE: accept 2nd dimension (multi-series) 901 Points beyond duration are removed onPlot.""" 902 for x, y in izip(self._now + self._sampling * numpy.arange(len(arr)), arr): 903 self.fresh.append((x, y)) 904 self._now += self._sampling * len(arr) 905 self.redraw_canvas(INVALID_PLOT)
906 - def clear(self):
907 self._now = 0.0 908 self._drawn = 1e10 909 self.past.clear(); self.seen.clear(); self.fresh.clear() 910 self._lastmap = None # force re-plot 911 self.redraw_canvas(INVALID_PLOT)
912 - def get_data(self):
913 """Returns a list of all visible datapoints.""" 914 data = [] 915 for deq in (self.past, self.seen, self.fresh): 916 for x in deq: 917 data.append(x) 918 return data
919 - def _onPlot(self, plot, context, fullyInvalid, w, h, xPix, yPix):
920 if 0 == (len(self.past) + len(self.seen) + len(self.fresh)): 921 context.set_source_rgb(*self.bgColor) 922 context.paint() 923 return 924 925 sampling = self._sampling 926 samples = int(round(self._target_dur / sampling)) 927 wps = w * 1.0 / samples 928 if wps >= 1.5: 929 samples = w / int(round(wps)) 930 elif wps >= .5: 931 samples = w 932 else: 933 samples = w * int(round(samples / w)) 934 eff_dur = samples * sampling 935 936 inow = int(round(self._now / sampling)) 937 idrawn = int(round(self._drawn / sampling)) 938 scroll = (inow - idrawn) * w / samples 939 horizon = self._now - eff_dur 940 941 #print 'scroll',scroll,'\tw =',w 942 #print self.duration,':',eff_dur,'\t',self._now,'since',self._drawn,'\tin samples:',inow,'to',idrawn,'\tscrolling',scroll,'\thorizon',horizon 943 944 y0 = self.yAxis.lower 945 dy = self.yAxis.upper - y0 946 if dy <= 0: 947 dy = 1.0 948 y1 = y0 + dy 949 950 while len(self.past) and (self.past[0][0] < horizon): 951 self.past.popleft() 952 953 def Quantize(pts): 954 if len(pts) == 0: return [],[] 955 xx = numpy.array([p[0] for p in pts]) 956 #print len(xx),'points from', xx[0],'to', xx[-1],'\tdrawn from\t', 957 xx -= horizon 958 xx *= w / eff_dur 959 xx = xx.astype(int) 960 #print xx[0],'to', xx[-1] 961 yy = numpy.array([p[1] for p in pts]) 962 yy -= y0 963 yy *= h/dy 964 yy = h - yy 965 return xx, yy
966 967 def Plot(xx, yy): 968 if len(xx): 969 PlotMany(context, xPix, yPix, xx, yy, self.dataColor)
970 971 if fullyInvalid or ((w, h) != self._lastdim) or (self._now < self._drawn): 972 self._lastdim = (w, h) 973 for c in [self._map_last.cairo_create(), context]: 974 c.set_source_rgb(*self.bgColor) 975 c.paint() 976 'fully' 977 Plot(*Quantize(self.past)) 978 Plot(*Quantize(self.seen)) 979 else: 980 context.set_source_pixmap(self._map_last, -scroll, 0) 981 context.rectangle(0, 0, w-scroll-1, h) 982 context.fill() 983 context.set_source_rgb(*self.bgColor) 984 context.rectangle(w-scroll, 0, w, h) 985 context.fill() 986 if scroll: 987 xx, yy = Quantize(self.seen) 988 i = 0 989 while len(self.seen) and (xx[i] < w-scroll-1): 990 self.past.append(self.seen.popleft()) 991 i += 1 992 while len(self.seen): 993 self.fresh.appendleft(self.seen.pop()) 994 xx, yy = Quantize(self.fresh) 995 Plot(xx, yy) 996 i = 0 997 while len(self.fresh) and (xx[i] < w-1): 998 self.past.append(self.fresh.popleft()) 999 i += 1 1000 while len(self.fresh): 1001 self.seen.append(self.fresh.popleft()) 1002 self._drawn = self._now 1003 1004
1005 -class StaticPlot(RulerPlot):
1006 """ 1007 Specialization of RulerPlot for (x,y) data given all-at-once. 1008 1009 @ivar yBounds: 1010 @ivar data: (list of x data points, list of y data points) 1011 or just the y list, with integer x coords 1012 """
1013 - def __init__(self, yBounds=None, connected=False, **kw):
1014 """ 1015 @param yBounds: (y_low, y_high) 1016 @param connected: def False (draws dots not lines) 1017 """ 1018 RulerPlot.__init__(self, **kw) 1019 self.set_size_request(100, 70) 1020 self.xx, self.yy, self.yy2 = [], [], [] 1021 self._setYBounds(yBounds) 1022 self.connected = connected 1023 self.r = Reffer() 1024 self.OnPlot += self.r(self._onPlot)
1025
1026 - def _setYBounds(self, yBounds):
1027 self._yBounds = yBounds 1028 if yBounds: 1029 self.yAxis.lower = yBounds[0] 1030 self.yAxis.upper = yBounds[1] 1031 self.yAxis.emit('changed')
1032 yBounds = property(lambda s: s._yBounds, _setYBounds) 1033
1034 - def _setData(self, data):
1035 try: 1036 x = data[0][0] # look for depth of structure 1037 self.xx = data[0] 1038 self.yy = data[1] 1039 self.yy2 = (len(data) > 2) and data[2:] or [] 1040 except: 1041 self.xx = range(len(data)) 1042 self.yy = data 1043 self.yy2 = [] 1044 if len(self.xx) >= 2: 1045 self.xAxis.lower = self.xx[0] 1046 self.xAxis.upper = self.xx[-1] 1047 self.xAxis.emit('changed') 1048 self.redraw_canvas(INVALID_PLOT)
1049 data = property(lambda s: s.yy, _setData)
1050 - def _onPlot(self, plot, context, fullyInvalid, w, h, xPix, yPix):
1051 context.save() 1052 x0, y0, x1, y1 = plot.xAxis.lower, plot.yAxis.upper, plot.xAxis.upper, plot.yAxis.lower 1053 if x1 == x0: x1 = x0 + 1 1054 if y1 == y0: y1 = y0 - 1 1055 xPix, yPix = self.transform(context, 0, 0, w, h, x0, y0, x1, y1, xPix, yPix) 1056 1057 # zero-line 1058 context.set_source_rgb(*self.fgColor) 1059 self.set_line_width(context, yPix, .5) 1060 context.move_to(plot.xAxis.lower, 0) 1061 context.line_to(plot.xAxis.upper, 0) 1062 context.stroke() 1063 1064 context.set_source_rgb(*self.dataColor) 1065 cur_color = self.dataColor 1066 if len(self.xx) > (3 * w): 1067 plot = lambda context, xPix, yPix, xx, yy, fat: PlotMany(context, xPix, yPix, xx, yy, cur_color) 1068 else: 1069 plot = self.connected and PlotLines or PlotDots 1070 plot(context, xPix, yPix, self.xx, self.yy, self.fat) 1071 1072 context.set_source_rgb(*self.dataColor2) 1073 cur_color = self.dataColor2 1074 for extra_yy in self.yy2: 1075 plot(context, xPix, yPix, self.xx, extra_yy, self.fat) 1076 1077 context.restore()
1078
1079 -def PlotDotsPairs(context, xPix, yPix, data, thick=1.0):
1080 """ 1081 Plots (x,y) in data on a cairo context as rectangles with "radius" of thick px. 1082 1083 @param xPix: width of a pixel 1084 @param yPix: height of a pixel 1085 """ 1086 for x, y in data: 1087 context.rectangle(x-thick*xPix, y-thick*yPix, 2*thick*xPix, 2*thick*yPix) 1088 context.fill()
1089 -def PlotDots(context, xPix, yPix, xx, yy, thick=1.0):
1090 """Plots (xx[i], yy[i]) on a cairo context as rectangles with "radius" of thick px.""" 1091 PlotDotsPairs(context, xPix, yPix, izip(xx, yy), thick)
1092 1093 1094 # cairo does not like to stroke a path millions of line segments long, so we break it down: 1095 #def PlotLinesPairs(context, xPix, yPix, data, thick=1.0): 1096 # first = True 1097 # context.save() 1098 # context.scale(xPix, yPix) 1099 # context.set_line_width(2*thick) 1100 # for x, y in data: 1101 # if first: 1102 # first = False 1103 # context.move_to(x/xPix, y/yPix) 1104 # else: 1105 # context.line_to(x/xPix, y/yPix) 1106 # context.stroke() 1107 # context.restore() 1108
1109 -def PlotLinesPairs(context, xPix, yPix, data, thick=1.0):
1110 """Plots (x,y) in data on a cairo context as a line series with line_width of thick px.""" 1111 atatime = 10000 1112 at = -1 1113 context.save() 1114 context.scale(xPix, yPix) 1115 context.set_line_width(2*thick) 1116 for x, y in data: 1117 if at < 0: 1118 context.move_to(x/xPix, y/yPix) 1119 at = 1 1120 continue 1121 context.line_to(x/xPix, y/yPix) 1122 at += 1 1123 if at == atatime: 1124 context.stroke() 1125 at = 0 1126 if at: 1127 context.stroke() 1128 context.restore()
1129 -def PlotLines(context, xPix, yPix, xx, yy, thick=1.0):
1130 """Plots (xx[i],yy[i]) on a cairo context as a line series with line_width of thick px.""" 1131 PlotLinesPairs(context, xPix, yPix, izip(xx, yy), thick)
1132 -def PlotLinesBounded(context, xPix, yPix, xx, yy, ymin, ymax, thick=1.0):
1133 """ 1134 Plots (xx[i],yy[i]) on a cairo context as a line series with line_width of thick px. 1135 yy[i] is brought within yBounds.""" 1136 def Pairs(): 1137 for x,y in izip(xx, yy): 1138 if ymin < y < ymax: 1139 yield x,y 1140 else: 1141 yield x, min(ymax, max(ymin, y))
1142 PlotLinesPairs(context, xPix, yPix, Pairs(), thick) 1143 1144 1145 HIST_WIDTH = .8 1146
1147 -def PlotHist(context, xPix, yPix, xx, yy, thick=1.0, hist_width=HIST_WIDTH):
1148 """Plots (xx[i], yy[i]) on a cairo context as histogram bins with line_width of thick px.""" 1149 N = min(len(xx), len(yy)) 1150 if N <= 1: 1151 return # how wide is a bin? 1152 context.save() 1153 context.scale(xPix, yPix) 1154 context.set_line_width(1*thick) 1155 for i in xrange(N): 1156 x, y = xx[i], yy[i] 1157 py = y/yPix 1158 if i == 0: 1159 px = (x - 0.5*(xx[1] - x)*hist_width) / xPix 1160 else: 1161 px = (x - 0.5*(x - xx[i-1])*hist_width) / xPix 1162 context.move_to(px, 0) 1163 context.line_to(px, py) 1164 if i == N-1: 1165 px = (x + 0.5*(x - xx[i-1])*hist_width) / xPix 1166 else: 1167 px = (x + 0.5*(xx[i+1] - x)*hist_width) / xPix 1168 context.line_to(px, py) 1169 context.line_to(px, 0) 1170 context.stroke() 1171 context.restore()
1172 1173
1174 -def PlotMany(context, xPix, yPix, xx, yy, dataColor=(1,1,1), with_std=True):
1175 if not have_numpy: return PlotLines(context, xPix, yPix, xx, yy) 1176 N = len(xx) 1177 if type(xx) != numpy.ndarray: 1178 xx = array(xx) 1179 if type(yy) != numpy.ndarray: 1180 yy = array(yy) 1181 xxi = xx * (1.0 / xPix) 1182 xxi += 0.5 - xx[0] / xPix 1183 xxi = xxi.astype(int) 1184 1185 context.save() 1186 context.translate(xx[0], 0) 1187 context.scale(xPix, yPix) 1188 context.translate(0.5, 0.0) 1189 context.set_line_width(1.0) 1190 dataGrey = tuple(list(dataColor)+[0.7]) 1191 dataColor = tuple(list(dataColor)+[1.0]) 1192 context.set_source_rgba(*dataColor) 1193 def VPlot(i, yLow, yHi): 1194 y0, y1 = yLow/yPix, yHi/yPix 1195 if abs(y0 - y1) < 1: 1196 mean = (y0 + y1) / 2 1197 y0, y1 = mean - .5, mean + .5 1198 context.move_to(i, y0) 1199 context.line_to(i, y1) 1200 context.stroke()
1201 1202 j = k = 0 1203 for i in xrange(xxi[-1]+1): 1204 while j < N and xxi[j] < i: 1205 j += 1 1206 if j == N or xxi[j] > i: 1207 continue 1208 k = j + 1 1209 while k < N and xxi[k] == i: 1210 k += 1 1211 pixrange = yy[j:k] 1212 if with_std: 1213 mean = pixrange.mean() 1214 std = pixrange.std() or yPix 1215 context.set_source_rgba(*dataGrey) 1216 VPlot(i, min(pixrange), max(pixrange)) 1217 context.set_source_rgba(*dataColor) 1218 VPlot(i, mean-std, mean+std) 1219 else: 1220 VPlot(i, min(pixrange), max(pixrange)) 1221 j = k 1222 context.restore() 1223 1224 1225
1226 -def ScaleUnit(value, sUnit=''):
1227 """ 1228 Returns a scaled value of "val" as function return with a single character unit 1229 scaler from the metric scale set as a tuple of the form (Scaled Value, Scaler Char+sUnit)""" 1230 if value: 1231 SuffixIndex=int(floor(log10(abs(value))/3)) 1232 if SuffixIndex!=0: 1233 1234 #Tack on the scale character. Note that SuffixIndex will be -1 for milli, -2 1235 #for micro, etc. Since 'm' is the 6th character in the suffix array, we need 1236 #to add 6 to get -1 (for milli) to point at the 6th character. It also means that 1237 #we must limit the index to be between -6 (+6=0, the first character) and 4 1238 #(+6=10, the last character). 1239 1240 SuffixIndex = EnsureRange(SuffixIndex,-6,4) 1241 return (value*10**(-3*SuffixIndex),'afpnum%KMGT'[SuffixIndex+6]+sUnit); 1242 return (value,sUnit)
1243
1244 -def EnsureRange(value,vLow,vHigh):
1245 """Returns value moved within (vLow, vHigh).""" 1246 if value<vLow:return vLow 1247 elif value>vHigh: return vHigh 1248 return value
1249