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