Package Bio :: Package Graphics :: Package GenomeDiagram :: Module _AbstractDrawer
[hide private]
[frames] | no frames]

Source Code for Module Bio.Graphics.GenomeDiagram._AbstractDrawer

  1  # Copyright 2003-2008 by Leighton Pritchard.  All rights reserved. 
  2  # Revisions copyright 2008-2009 by Peter Cock. 
  3  # This code is part of the Biopython distribution and governed by its 
  4  # license.  Please see the LICENSE file that should have been included 
  5  # as part of this package. 
  6  # 
  7  # Contact:       Leighton Pritchard, Scottish Crop Research Institute, 
  8  #                Invergowrie, Dundee, Scotland, DD2 5DA, UK 
  9  #                L.Pritchard@scri.ac.uk 
 10  ################################################################################ 
 11   
 12  """AbstractDrawer module (considered to be a private module, the API may change!) 
 13   
 14  Provides: 
 15   
 16   - AbstractDrawer -    Superclass for methods common to the Drawer objects 
 17   
 18   - page_sizes -          Method that returns a ReportLab pagesize when passed 
 19     a valid ISO size 
 20   
 21   - draw_box -            Method that returns a closed path object when passed 
 22     the proper co-ordinates.  For HORIZONTAL boxes only. 
 23   
 24   - angle2trig -          Method that returns a tuple of values that are the 
 25     vector for rotating a point through a passed angle, 
 26     about an origin 
 27   
 28   - intermediate_points - Method that returns a list of values intermediate 
 29     between the points in a passed dataset 
 30   
 31  For drawing capabilities, this module uses reportlab to draw and write 
 32  the diagram: http://www.reportlab.com 
 33   
 34  For dealing with biological information, the package expects Biopython objects 
 35  like SeqFeatures. 
 36  """ 
 37   
 38  # ReportLab imports 
 39  from __future__ import print_function 
 40   
 41  from Bio._py3k import range 
 42   
 43  from reportlab.lib import pagesizes 
 44  from reportlab.lib import colors 
 45  from reportlab.graphics.shapes import * 
 46   
 47  from math import pi 
 48   
 49  __docformat__ = "restructuredtext en" 
 50   
 51  ################################################################################ 
 52  # METHODS 
 53  ################################################################################ 
 54  # Utility method to translate strings to ISO page sizes 
55 -def page_sizes(size):
56 """Convert size string into a Reportlab pagesize. 57 58 Arguments: 59 60 - size - A string representing a standard page size, eg 'A4' or 'LETTER' 61 """ 62 sizes = {'A0': pagesizes.A0, # ReportLab pagesizes, keyed by ISO string 63 'A1': pagesizes.A1, 64 'A2': pagesizes.A2, 65 'A3': pagesizes.A3, 66 'A4': pagesizes.A4, 67 'A5': pagesizes.A5, 68 'A6': pagesizes.A6, 69 'B0': pagesizes.B0, 70 'B1': pagesizes.B1, 71 'B2': pagesizes.B2, 72 'B3': pagesizes.B3, 73 'B4': pagesizes.B4, 74 'B5': pagesizes.B5, 75 'B6': pagesizes.B6, 76 'ELEVENSEVENTEEN': pagesizes.ELEVENSEVENTEEN, 77 'LEGAL': pagesizes.LEGAL, 78 'LETTER': pagesizes.LETTER 79 } 80 try: 81 return sizes[size] 82 except: 83 raise ValueError("%s not in list of page sizes" % size)
84 85
86 -def _stroke_and_fill_colors(color, border):
87 """Helper function handle border and fill colors (PRIVATE).""" 88 if not isinstance(color, colors.Color): 89 raise ValueError("Invalid color %r" % color) 90 91 if color == colors.white and border is None: # Force black border on 92 strokecolor = colors.black # white boxes with 93 elif border is None: # undefined border, else 94 strokecolor = color # use fill color 95 elif border: 96 if not isinstance(border, colors.Color): 97 raise ValueError("Invalid border color %r" % border) 98 strokecolor = border 99 else: 100 # e.g. False 101 strokecolor = None 102 103 return strokecolor, color
104 105
106 -def draw_box(point1, point2, 107 color=colors.lightgreen, border=None, colour=None, 108 **kwargs):
109 """Draw a box. 110 111 Arguments: 112 113 - point1, point2 - coordinates for opposite corners of the box 114 (x,y tuples) 115 - color /colour - The color for the box 116 (colour takes priority over color) 117 - border - Border color for the box 118 119 Returns a closed path object, beginning at (x1,y1) going round 120 the four points in order, and filling with the passed color. 121 """ 122 x1, y1 = point1 123 x2, y2 = point2 124 125 # Let the UK spelling (colour) override the USA spelling (color) 126 if colour is not None: 127 color = colour 128 del colour 129 130 strokecolor, color = _stroke_and_fill_colors(color, border) 131 132 x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2) 133 return Polygon([x1, y1, x2, y1, x2, y2, x1, y2], 134 strokeColor=strokecolor, 135 fillColor=color, 136 strokewidth=0, 137 **kwargs)
138 139
140 -def draw_cut_corner_box(point1, point2, corner=0.5, 141 color=colors.lightgreen, border=None, **kwargs):
142 """Draw a box with the corners cut off.""" 143 x1, y1 = point1 144 x2, y2 = point2 145 146 if not corner: 147 return draw_box(point1, point2, color, border) 148 elif corner < 0: 149 raise ValueError("Arrow head length ratio should be positive") 150 151 strokecolor, color = _stroke_and_fill_colors(color, border) 152 153 boxheight = y2-y1 154 boxwidth = x2-x1 155 corner = min(boxheight*0.5, boxheight*0.5*corner) 156 157 return Polygon([x1, y1+corner, 158 x1, y2-corner, 159 x1+corner, y2, 160 x2-corner, y2, 161 x2, y2-corner, 162 x2, y1+corner, 163 x2-corner, y1, 164 x1+corner, y1], 165 strokeColor=strokecolor, 166 strokeWidth=1, 167 strokeLineJoin=1, # 1=round 168 fillColor=color, 169 **kwargs)
170 171
172 -def draw_polygon(list_of_points, 173 color=colors.lightgreen, border=None, colour=None, 174 **kwargs):
175 """Draw polygon. 176 177 Arguments: 178 179 - list_of_point - list of (x,y) tuples for the corner coordinates 180 - color / colour - The color for the box 181 182 Returns a closed path object, beginning at (x1,y1) going round 183 the four points in order, and filling with the passed colour. 184 """ 185 # Let the UK spelling (colour) override the USA spelling (color) 186 if colour is not None: 187 color = colour 188 del colour 189 190 strokecolor, color = _stroke_and_fill_colors(color, border) 191 192 xy_list = [] 193 for (x, y) in list_of_points: 194 xy_list.append(x) 195 xy_list.append(y) 196 197 return Polygon(xy_list, 198 strokeColor=strokecolor, 199 fillColor=color, 200 strokewidth=0, 201 **kwargs)
202 203
204 -def draw_arrow(point1, point2, color=colors.lightgreen, border=None, 205 shaft_height_ratio=0.4, head_length_ratio=0.5, orientation='right', 206 colour=None, **kwargs):
207 """Draw an arrow. 208 209 Returns a closed path object representing an arrow enclosed by the 210 box with corners at {point1=(x1,y1), point2=(x2,y2)}, a shaft height 211 given by shaft_height_ratio (relative to box height), a head length 212 given by head_length_ratio (also relative to box height), and 213 an orientation that may be 'left' or 'right'. 214 """ 215 x1, y1 = point1 216 x2, y2 = point2 217 218 if shaft_height_ratio < 0 or 1 < shaft_height_ratio: 219 raise ValueError("Arrow shaft height ratio should be in range 0 to 1") 220 if head_length_ratio < 0: 221 raise ValueError("Arrow head length ratio should be positive") 222 223 # Let the UK spelling (colour) override the USA spelling (color) 224 if colour is not None: 225 color = colour 226 del colour 227 228 strokecolor, color = _stroke_and_fill_colors(color, border) 229 230 # Depending on the orientation, we define the bottom left (x1, y1) and 231 # top right (x2, y2) coordinates differently, but still draw the box 232 # using the same relative co-ordinates: 233 xmin, ymin = min(x1, x2), min(y1, y2) 234 xmax, ymax = max(x1, x2), max(y1, y2) 235 if orientation == 'right': 236 x1, x2, y1, y2 = xmin, xmax, ymin, ymax 237 elif orientation == 'left': 238 x1, x2, y1, y2 = xmax, xmin, ymin, ymax 239 else: 240 raise ValueError("Invalid orientation %s, should be 'left' or 'right'" 241 % repr(orientation)) 242 243 # We define boxheight and boxwidth accordingly, and calculate the shaft 244 # height from these. We also ensure that the maximum head length is 245 # the width of the box enclosure 246 boxheight = y2-y1 247 boxwidth = x2-x1 248 shaftheight = boxheight*shaft_height_ratio 249 headlength = min(abs(boxheight)*head_length_ratio, abs(boxwidth)) 250 if boxwidth < 0: 251 headlength *= -1 # reverse it 252 253 shafttop = 0.5*(boxheight+shaftheight) 254 shaftbase = boxheight-shafttop 255 headbase = boxwidth-headlength 256 midheight = 0.5*boxheight 257 return Polygon([x1, y1+shafttop, 258 x1+headbase, y1+shafttop, 259 x1+headbase, y2, 260 x2, y1+midheight, 261 x1+headbase, y1, 262 x1+headbase, y1+shaftbase, 263 x1, y1+shaftbase], 264 strokeColor=strokecolor, 265 # strokeWidth=max(1, int(boxheight/40.)), 266 strokeWidth=1, 267 # default is mitre/miter which can stick out too much: 268 strokeLineJoin=1, # 1=round 269 fillColor=color, 270 **kwargs)
271 272
273 -def angle2trig(theta):
274 """Convert anngle to a reportlab ready tuple. 275 276 Arguments: 277 278 - theta - Angle in degrees, counter clockwise from horizontal 279 280 Returns a representation of the passed angle in a format suitable 281 for ReportLab rotations (i.e. cos(theta), sin(theta), -sin(theta), 282 cos(theta) tuple) 283 """ 284 c = cos(theta * pi / 180) 285 s = sin(theta * pi / 180) 286 return(c, s, -s, c) # Vector for rotating point around an origin
287 288
289 -def intermediate_points(start, end, graph_data):
290 """Generate intermediate points describing provided graph data.. 291 292 Returns a list of (start, end, value) tuples describing the passed 293 graph data as 'bins' between position midpoints. 294 """ 295 # print start, end, len(graph_data) 296 newdata = [] # data in form (X0, X1, val) 297 # add first block 298 newdata.append((start, graph_data[0][0]+(graph_data[1][0]-graph_data[0][0])/2., 299 graph_data[0][1])) 300 # add middle set 301 for index in range(1, len(graph_data)-1): 302 lastxval, lastyval = graph_data[index-1] 303 xval, yval = graph_data[index] 304 nextxval, nextyval = graph_data[index+1] 305 newdata.append((lastxval+(xval-lastxval)/2., 306 xval+(nextxval-xval)/2., yval)) 307 # add last block 308 newdata.append((xval+(nextxval-xval)/2., 309 end, graph_data[-1][1])) 310 # print newdata[-1] 311 # print newdata 312 return newdata
313 314 ################################################################################ 315 # CLASSES 316 ################################################################################ 317 318
319 -class AbstractDrawer(object):
320 """AbstractDrawer 321 322 Provides: 323 324 Methods: 325 326 - __init__(self, parent, pagesize='A3', orientation='landscape', 327 x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None, 328 start=None, end=None, tracklines=0) Called on instantiation 329 330 - set_page_size(self, pagesize, orientation) Set the page size to the 331 passed size and orientation 332 333 - set_margins(self, x, y, xl, xr, yt, yb) Set the drawable area of the 334 page 335 336 - set_bounds(self, start, end) Set the bounds for the elements to be 337 drawn 338 339 - is_in_bounds(self, value) Returns a boolean for whether the position 340 is actually to be drawn 341 342 - __len__(self) Returns the length of sequence that will be drawn 343 344 Attributes: 345 346 - tracklines Boolean for whether to draw lines dilineating tracks 347 348 - pagesize Tuple describing the size of the page in pixels 349 350 - x0 Float X co-ord for leftmost point of drawable area 351 352 - xlim Float X co-ord for rightmost point of drawable area 353 354 - y0 Float Y co-ord for lowest point of drawable area 355 356 - ylim Float Y co-ord for topmost point of drawable area 357 358 - pagewidth Float pixel width of drawable area 359 360 - pageheight Float pixel height of drawable area 361 362 - xcenter Float X co-ord of center of drawable area 363 364 - ycenter Float Y co-ord of center of drawable area 365 366 - start Int, base to start drawing from 367 368 - end Int, base to stop drawing at 369 370 - length Size of sequence to be drawn 371 372 - cross_track_links List of tuples each with four entries (track A, 373 feature A, track B, feature B) to be linked. 374 """
375 - def __init__(self, parent, pagesize='A3', orientation='landscape', 376 x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None, 377 start=None, end=None, tracklines=0, cross_track_links=None):
378 """Create the object. 379 380 Arguments: 381 382 - parent Diagram object containing the data that the drawer 383 draws 384 385 - pagesize String describing the ISO size of the image, or a tuple 386 of pixels 387 388 - orientation String describing the required orientation of the 389 final drawing ('landscape' or 'portrait') 390 391 - x Float (0->1) describing the relative size of the X 392 margins to the page 393 394 - y Float (0->1) describing the relative size of the Y 395 margins to the page 396 397 - xl Float (0->1) describing the relative size of the left X 398 margin to the page (overrides x) 399 400 - xl Float (0->1) describing the relative size of the left X 401 margin to the page (overrides x) 402 403 - xr Float (0->1) describing the relative size of the right X 404 margin to the page (overrides x) 405 406 - yt Float (0->1) describing the relative size of the top Y 407 margin to the page (overrides y) 408 409 - yb Float (0->1) describing the relative size of the lower Y 410 margin to the page (overrides y) 411 412 - start Int, the position to begin drawing the diagram at 413 414 - end Int, the position to stop drawing the diagram at 415 416 - tracklines Boolean flag to show (or not) lines delineating tracks 417 on the diagram 418 419 - cross_track_links List of tuples each with four entries (track A, 420 feature A, track B, feature B) to be linked. 421 """ 422 self._parent = parent # The calling Diagram object 423 424 # Perform 'administrative' tasks of setting up the page 425 self.set_page_size(pagesize, orientation) # Set drawing size 426 self.set_margins(x, y, xl, xr, yt, yb) # Set page margins 427 self.set_bounds(start, end) # Set limits on what will be drawn 428 self.tracklines = tracklines # Set flags 429 if cross_track_links is None: 430 cross_track_links = [] 431 else: 432 self.cross_track_links = cross_track_links
433
434 - def set_page_size(self, pagesize, orientation):
435 """Set page size of the drawing.. 436 437 Arguments: 438 439 - pagesize Size of the output image, a tuple of pixels (width, 440 height, or a string in the reportlab.lib.pagesizes 441 set of ISO sizes. 442 443 - orientation String: 'landscape' or 'portrait' 444 """ 445 if isinstance(pagesize, str): # A string, so translate 446 pagesize = page_sizes(pagesize) 447 elif isinstance(pagesize, tuple): # A tuple, so don't translate 448 pagesize = pagesize 449 else: 450 raise ValueError("Page size %s not recognised" % pagesize) 451 shortside, longside = min(pagesize), max(pagesize) 452 453 orientation = orientation.lower() 454 if orientation not in ('landscape', 'portrait'): 455 raise ValueError("Orientation %s not recognised" % orientation) 456 if orientation == 'landscape': 457 self.pagesize = (longside, shortside) 458 else: 459 self.pagesize = (shortside, longside)
460
461 - def set_margins(self, x, y, xl, xr, yt, yb):
462 """Set page margins. 463 464 Arguments: 465 466 - x Float(0->1), Absolute X margin as % of page 467 - y Float(0->1), Absolute Y margin as % of page 468 - xl Float(0->1), Left X margin as % of page 469 - xr Float(0->1), Right X margin as % of page 470 - yt Float(0->1), Top Y margin as % of page 471 - yb Float(0->1), Bottom Y margin as % of page 472 473 Set the page margins as proportions of the page 0->1, and also 474 set the page limits x0, y0 and xlim, ylim, and page center 475 xorigin, yorigin, as well as overall page width and height 476 """ 477 # Set left, right, top and bottom margins 478 xmargin_l = xl or x 479 xmargin_r = xr or x 480 ymargin_top = yt or y 481 ymargin_btm = yb or y 482 483 # Set page limits, center and height/width 484 self.x0, self.y0 = self.pagesize[0]*xmargin_l, self.pagesize[1]*ymargin_btm 485 self.xlim, self.ylim = self.pagesize[0]*(1-xmargin_r), self.pagesize[1]*(1-ymargin_top) 486 self.pagewidth = self.xlim-self.x0 487 self.pageheight = self.ylim-self.y0 488 self.xcenter, self.ycenter = self.x0+self.pagewidth/2., self.y0+self.pageheight/2.
489
490 - def set_bounds(self, start, end):
491 """Set start and end points for the drawing as a whole. 492 493 Arguments: 494 495 - start - The first base (or feature mark) to draw from 496 - end - The last base (or feature mark) to draw to 497 """ 498 low, high = self._parent.range() # Extent of tracks 499 500 if start is not None and end is not None and start > end: 501 start, end = end, start 502 503 if start is None or start < 0: # Check validity of passed args and 504 start = 0 # default to 0 505 if end is None or end < 0: 506 end = high + 1 # default to track range top limit 507 508 self.start, self.end = int(start), int(end) 509 self.length = self.end - self.start + 1
510
511 - def is_in_bounds(self, value):
512 """Check if given value is withing the region selected for drawing, 513 514 Arguments: 515 516 - value - A base position 517 """ 518 if value >= self.start and value <= self.end: 519 return 1 520 return 0
521
522 - def __len__(self):
523 """Returns the length of the region to be drawn.""" 524 return self.length
525
526 - def _current_track_start_end(self):
527 track = self._parent[self.current_track_level] 528 if track.start is None: 529 start = self.start 530 else: 531 start = max(self.start, track.start) 532 if track.end is None: 533 end = self.end 534 else: 535 end = min(self.end, track.end) 536 return start, end
537