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
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
67
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
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
135 self._dataColor2 = dataColor2
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()
157 self.OnOverlay = WeakEvent()
158 self.OnUserXDraw = WeakEvent()
159 self.OnUserYDraw = WeakEvent()
160 self.OnPress = WeakEvent()
161 self.OnRelease = WeakEvent()
162 self.OnDblClick = WeakEvent()
163 self.OnDrag = WeakEvent()
164 self.OnRoll = WeakEvent()
165 self.OnPopup = WeakEvent()
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
178
179
180 margin = property(lambda s: s._margin)
186 bgColor = property(lambda s: s._bgColor, _setBgColor)
190 fgColor = property(lambda s: s._fgColor, _setFgColor)
191
196 dataColor = property(lambda s: s._dataColor, _setDataColor)
197
198
202 dataColor2 = property(lambda s: s._dataColor2, _setDataColor2)
203
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)
218 pointColor = property(lambda s: s._pointColor, _setPointColor)
219
220
221
222
223
226 popup = property(lambda s: s._popup, _setPopup)
230 showHint = property(lambda s: s._showHint, _setShowHint)
235 hint = property(lambda s: s._hint, _setHint)
236
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)
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])
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()
285
286
287
288
289
311
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)
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()
324 self.hint = '(%s, %s)' % (self._rule_xformat(x), self._rule_yformat(y))
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)
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)
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
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
607 """Part of the printing line-width kluge."""
608 if self.unit_width:
609 context.set_line_width(points*self.fat*self.unit_width)
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
655 context.save()
656 context.translate(x, baseline)
657 context.scale(xPix, yPix)
658 context.scale(1.0, -1.0)
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()
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()
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]
696 def drawLbl(x, lbl):
697
698 context.save()
699 context.translate(x, baseline)
700 context.scale(xPix, yPix)
701 context.scale(1.0, -1.0)
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
847
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
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
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
1035 try:
1036 x = data[0][0]
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
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
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
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
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)
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
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
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
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
1235
1236
1237
1238
1239
1240 SuffixIndex = EnsureRange(SuffixIndex,-6,4)
1241 return (value*10**(-3*SuffixIndex),'afpnum%KMGT'[SuffixIndex+6]+sUnit);
1242 return (value,sUnit)
1243
1245 """Returns value moved within (vLow, vHigh)."""
1246 if value<vLow:return vLow
1247 elif value>vHigh: return vHigh
1248 return value
1249