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