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