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 x_corner = min(boxheight * 0.5 * corner, boxwidth * 0.5) 151 y_corner = min(boxheight * 0.5 * corner, boxheight * 0.5) 152 153 return Polygon([x1, y1 + y_corner, 154 x1, y2 - y_corner, 155 x1 + x_corner, y2, 156 x2 - x_corner, y2, 157 x2, y2 - y_corner, 158 x2, y1 + y_corner, 159 x2 - x_corner, y1, 160 x1 + x_corner, y1], 161 strokeColor=strokecolor, 162 strokeWidth=1, 163 strokeLineJoin=1, # 1=round 164 fillColor=color, 165 **kwargs)
166 167
168 -def draw_polygon(list_of_points, 169 color=colors.lightgreen, border=None, colour=None, 170 **kwargs):
171 """Draw polygon. 172 173 Arguments: 174 - list_of_point - list of (x,y) tuples for the corner coordinates 175 - color / colour - The color for the box 176 177 Returns a closed path object, beginning at (x1,y1) going round 178 the four points in order, and filling with the passed colour. 179 180 """ 181 # Let the UK spelling (colour) override the USA spelling (color) 182 if colour is not None: 183 color = colour 184 del colour 185 186 strokecolor, color = _stroke_and_fill_colors(color, border) 187 188 xy_list = [] 189 for (x, y) in list_of_points: 190 xy_list.append(x) 191 xy_list.append(y) 192 193 return Polygon(xy_list, 194 strokeColor=strokecolor, 195 fillColor=color, 196 strokewidth=0, 197 **kwargs)
198 199
200 -def draw_arrow(point1, point2, color=colors.lightgreen, border=None, 201 shaft_height_ratio=0.4, head_length_ratio=0.5, orientation='right', 202 colour=None, **kwargs):
203 """Draw an arrow. 204 205 Returns a closed path object representing an arrow enclosed by the 206 box with corners at {point1=(x1,y1), point2=(x2,y2)}, a shaft height 207 given by shaft_height_ratio (relative to box height), a head length 208 given by head_length_ratio (also relative to box height), and 209 an orientation that may be 'left' or 'right'. 210 """ 211 x1, y1 = point1 212 x2, y2 = point2 213 214 if shaft_height_ratio < 0 or 1 < shaft_height_ratio: 215 raise ValueError("Arrow shaft height ratio should be in range 0 to 1") 216 if head_length_ratio < 0: 217 raise ValueError("Arrow head length ratio should be positive") 218 219 # Let the UK spelling (colour) override the USA spelling (color) 220 if colour is not None: 221 color = colour 222 del colour 223 224 strokecolor, color = _stroke_and_fill_colors(color, border) 225 226 # Depending on the orientation, we define the bottom left (x1, y1) and 227 # top right (x2, y2) coordinates differently, but still draw the box 228 # using the same relative co-ordinates: 229 xmin, ymin = min(x1, x2), min(y1, y2) 230 xmax, ymax = max(x1, x2), max(y1, y2) 231 if orientation == 'right': 232 x1, x2, y1, y2 = xmin, xmax, ymin, ymax 233 elif orientation == 'left': 234 x1, x2, y1, y2 = xmax, xmin, ymin, ymax 235 else: 236 raise ValueError("Invalid orientation %s, should be 'left' or 'right'" 237 % repr(orientation)) 238 239 # We define boxheight and boxwidth accordingly, and calculate the shaft 240 # height from these. We also ensure that the maximum head length is 241 # the width of the box enclosure 242 boxheight = y2 - y1 243 boxwidth = x2 - x1 244 shaftheight = boxheight * shaft_height_ratio 245 headlength = min(abs(boxheight) * head_length_ratio, abs(boxwidth)) 246 if boxwidth < 0: 247 headlength *= -1 # reverse it 248 249 shafttop = 0.5 * (boxheight + shaftheight) 250 shaftbase = boxheight - shafttop 251 headbase = boxwidth - headlength 252 midheight = 0.5 * boxheight 253 return Polygon([x1, y1 + shafttop, 254 x1 + headbase, y1 + shafttop, 255 x1 + headbase, y2, 256 x2, y1 + midheight, 257 x1 + headbase, y1, 258 x1 + headbase, y1 + shaftbase, 259 x1, y1 + shaftbase], 260 strokeColor=strokecolor, 261 # strokeWidth=max(1, int(boxheight/40.)), 262 strokeWidth=1, 263 # default is mitre/miter which can stick out too much: 264 strokeLineJoin=1, # 1=round 265 fillColor=color, 266 **kwargs)
267 268
269 -def angle2trig(theta):
270 """Convert angle to a reportlab ready tuple. 271 272 Arguments: 273 - theta - Angle in degrees, counter clockwise from horizontal 274 275 Returns a representation of the passed angle in a format suitable 276 for ReportLab rotations (i.e. cos(theta), sin(theta), -sin(theta), 277 cos(theta) tuple) 278 """ 279 c = cos(theta * pi / 180) 280 s = sin(theta * pi / 180) 281 return(c, s, -s, c) # Vector for rotating point around an origin
282 283
284 -def intermediate_points(start, end, graph_data):
285 """Generate intermediate points describing provided graph data.. 286 287 Returns a list of (start, end, value) tuples describing the passed 288 graph data as 'bins' between position midpoints. 289 """ 290 # print start, end, len(graph_data) 291 newdata = [] # data in form (X0, X1, val) 292 # add first block 293 newdata.append((start, 294 graph_data[0][0] + (graph_data[1][0] - graph_data[0][0]) / 2., 295 graph_data[0][1])) 296 # add middle set 297 for index in range(1, len(graph_data) - 1): 298 lastxval, lastyval = graph_data[index - 1] 299 xval, yval = graph_data[index] 300 nextxval, nextyval = graph_data[index + 1] 301 newdata.append((lastxval + (xval - lastxval) / 2., 302 xval + (nextxval - xval) / 2., yval)) 303 # add last block 304 newdata.append((xval + (nextxval - xval) / 2., 305 end, graph_data[-1][1])) 306 # print newdata[-1] 307 # print newdata 308 return newdata
309 310 ################################################################################ 311 # CLASSES 312 ################################################################################ 313 314
315 -class AbstractDrawer(object):
316 """Abstract Drawer. 317 318 Attributes: 319 - tracklines Boolean for whether to draw lines delineating tracks 320 - pagesize Tuple describing the size of the page in pixels 321 - x0 Float X co-ord for leftmost point of drawable area 322 - xlim Float X co-ord for rightmost point of drawable area 323 - y0 Float Y co-ord for lowest point of drawable area 324 - ylim Float Y co-ord for topmost point of drawable area 325 - pagewidth Float pixel width of drawable area 326 - pageheight Float pixel height of drawable area 327 - xcenter Float X co-ord of center of drawable area 328 - ycenter Float Y co-ord of center of drawable area 329 - start Int, base to start drawing from 330 - end Int, base to stop drawing at 331 - length Size of sequence to be drawn 332 - cross_track_links List of tuples each with four entries (track A, 333 feature A, track B, feature B) to be linked. 334 335 """ 336
337 - def __init__(self, parent, pagesize='A3', orientation='landscape', 338 x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None, 339 start=None, end=None, tracklines=0, cross_track_links=None):
340 """Create the object. 341 342 Arguments: 343 - parent Diagram object containing the data that the drawer draws 344 - pagesize String describing the ISO size of the image, or a tuple 345 of pixels 346 - orientation String describing the required orientation of the 347 final drawing ('landscape' or 'portrait') 348 - x Float (0->1) describing the relative size of the X 349 margins to the page 350 - y Float (0->1) describing the relative size of the Y 351 margins to the page 352 - xl Float (0->1) describing the relative size of the left X 353 margin to the page (overrides x) 354 - xl Float (0->1) describing the relative size of the left X 355 margin to the page (overrides x) 356 - xr Float (0->1) describing the relative size of the right X 357 margin to the page (overrides x) 358 - yt Float (0->1) describing the relative size of the top Y 359 margin to the page (overrides y) 360 - yb Float (0->1) describing the relative size of the lower Y 361 margin to the page (overrides y) 362 - start Int, the position to begin drawing the diagram at 363 - end Int, the position to stop drawing the diagram at 364 - tracklines Boolean flag to show (or not) lines delineating tracks 365 on the diagram 366 - cross_track_links List of tuples each with four entries (track A, 367 feature A, track B, feature B) to be linked. 368 369 """ 370 self._parent = parent # The calling Diagram object 371 372 # Perform 'administrative' tasks of setting up the page 373 self.set_page_size(pagesize, orientation) # Set drawing size 374 self.set_margins(x, y, xl, xr, yt, yb) # Set page margins 375 self.set_bounds(start, end) # Set limits on what will be drawn 376 self.tracklines = tracklines # Set flags 377 if cross_track_links is None: 378 cross_track_links = [] 379 else: 380 self.cross_track_links = cross_track_links
381
382 - def set_page_size(self, pagesize, orientation):
383 """Set page size of the drawing.. 384 385 Arguments: 386 - pagesize Size of the output image, a tuple of pixels (width, 387 height, or a string in the reportlab.lib.pagesizes 388 set of ISO sizes. 389 - orientation String: 'landscape' or 'portrait' 390 391 """ 392 if isinstance(pagesize, str): # A string, so translate 393 pagesize = page_sizes(pagesize) 394 elif isinstance(pagesize, tuple): # A tuple, so don't translate 395 pagesize = pagesize 396 else: 397 raise ValueError("Page size %s not recognised" % pagesize) 398 shortside, longside = min(pagesize), max(pagesize) 399 400 orientation = orientation.lower() 401 if orientation not in ('landscape', 'portrait'): 402 raise ValueError("Orientation %s not recognised" % orientation) 403 if orientation == 'landscape': 404 self.pagesize = (longside, shortside) 405 else: 406 self.pagesize = (shortside, longside)
407
408 - def set_margins(self, x, y, xl, xr, yt, yb):
409 """Set page margins. 410 411 Arguments: 412 - x Float(0->1), Absolute X margin as % of page 413 - y Float(0->1), Absolute Y margin as % of page 414 - xl Float(0->1), Left X margin as % of page 415 - xr Float(0->1), Right X margin as % of page 416 - yt Float(0->1), Top Y margin as % of page 417 - yb Float(0->1), Bottom Y margin as % of page 418 419 Set the page margins as proportions of the page 0->1, and also 420 set the page limits x0, y0 and xlim, ylim, and page center 421 xorigin, yorigin, as well as overall page width and height 422 """ 423 # Set left, right, top and bottom margins 424 xmargin_l = xl or x 425 xmargin_r = xr or x 426 ymargin_top = yt or y 427 ymargin_btm = yb or y 428 429 # Set page limits, center and height/width 430 self.x0, self.y0 = self.pagesize[0] * xmargin_l, self.pagesize[1] * ymargin_btm 431 self.xlim, self.ylim = self.pagesize[0] * (1 - xmargin_r), self.pagesize[1] * (1 - ymargin_top) 432 self.pagewidth = self.xlim - self.x0 433 self.pageheight = self.ylim - self.y0 434 self.xcenter, self.ycenter = self.x0 + self.pagewidth / 2., self.y0 + self.pageheight / 2.
435
436 - def set_bounds(self, start, end):
437 """Set start and end points for the drawing as a whole. 438 439 Arguments: 440 - start - The first base (or feature mark) to draw from 441 - end - The last base (or feature mark) to draw to 442 443 """ 444 low, high = self._parent.range() # Extent of tracks 445 446 if start is not None and end is not None and start > end: 447 start, end = end, start 448 449 if start is None or start < 0: # Check validity of passed args and 450 start = 0 # default to 0 451 if end is None or end < 0: 452 end = high + 1 # default to track range top limit 453 454 self.start, self.end = int(start), int(end) 455 self.length = self.end - self.start + 1
456
457 - def is_in_bounds(self, value):
458 """Check if given value is within the region selected for drawing. 459 460 Arguments: 461 - value - A base position 462 463 """ 464 if value >= self.start and value <= self.end: 465 return 1 466 return 0
467
468 - def __len__(self):
469 """Return the length of the region to be drawn.""" 470 return self.length
471
472 - def _current_track_start_end(self):
473 track = self._parent[self.current_track_level] 474 if track.start is None: 475 start = self.start 476 else: 477 start = max(self.start, track.start) 478 if track.end is None: 479 end = self.end 480 else: 481 end = min(self.end, track.end) 482 return start, end
483