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

Source Code for Module Bio.Graphics.GenomeDiagram._CircularDrawer

   1  # Copyright 2003-2008 by Leighton Pritchard.  All rights reserved. 
   2  # Revisions copyright 2008-2012 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  """CircularDrawer module for GenomeDiagram.""" 
  13   
  14  # ReportLab imports 
  15  from reportlab.graphics.shapes import * 
  16  from reportlab.lib import colors 
  17  from reportlab.pdfbase import _fontdata 
  18  from reportlab.graphics.shapes import ArcPath 
  19   
  20  # GenomeDiagram imports 
  21  from _AbstractDrawer import AbstractDrawer, draw_polygon, intermediate_points 
  22  from _AbstractDrawer import _stroke_and_fill_colors 
  23  from _FeatureSet import FeatureSet 
  24  from _GraphSet import GraphSet 
  25   
  26  from math import ceil, pi, cos, sin, asin 
  27   
  28   
29 -class CircularDrawer(AbstractDrawer):
30 """Object for drawing circular diagrams. 31 32 o __init__(self, ...) Called on instantiation 33 34 o set_page_size(self, pagesize, orientation) Set the page size to the 35 passed size and orientation 36 37 o set_margins(self, x, y, xl, xr, yt, yb) Set the drawable area of the 38 page 39 40 o set_bounds(self, start, end) Set the bounds for the elements to be 41 drawn 42 43 o is_in_bounds(self, value) Returns a boolean for whether the position 44 is actually to be drawn 45 46 o __len__(self) Returns the length of sequence that will be drawn 47 48 49 o draw(self) Place the drawing elements on the diagram 50 51 o init_fragments(self) Calculate information 52 about sequence fragment locations on the drawing 53 54 o set_track_heights(self) Calculate information about the offset of 55 each track from the fragment base 56 57 o draw_test_tracks(self) Add lines demarcating each track to the 58 drawing 59 60 o draw_track(self, track) Return the contents of the passed track as 61 drawing elements 62 63 o draw_scale(self, track) Return a scale for the passed track as 64 drawing elements 65 66 o draw_greytrack(self, track) Return a grey background and superposed 67 label for the passed track as drawing 68 elements 69 70 o draw_feature_set(self, set) Return the features in the passed set as 71 drawing elements 72 73 o draw_feature(self, feature) Return a single feature as drawing 74 elements 75 76 o get_feature_sigil(self, feature, x0, x1, fragment) Return a single 77 feature as its sigil in drawing elements 78 79 o draw_graph_set(self, set) Return the data in a set of graphs as 80 drawing elements 81 82 o draw_line_graph(self, graph) Return the data in a graph as a line 83 graph in drawing elements 84 85 o draw_heat_graph(self, graph) Return the data in a graph as a heat 86 graph in drawing elements 87 88 o draw_bar_graph(self, graph) Return the data in a graph as a bar 89 graph in drawing elements 90 91 o canvas_angle(self, base) Return the angle, and cos and sin of 92 that angle, subtended by the passed 93 base position at the diagram center 94 95 o draw_arc(self, inner_radius, outer_radius, startangle, endangle, 96 color) Return a drawable element describing an arc 97 98 Attributes: 99 100 o tracklines Boolean for whether to draw lines dilineating tracks 101 102 o pagesize Tuple describing the size of the page in pixels 103 104 o x0 Float X co-ord for leftmost point of drawable area 105 106 o xlim Float X co-ord for rightmost point of drawable area 107 108 o y0 Float Y co-ord for lowest point of drawable area 109 110 o ylim Float Y co-ord for topmost point of drawable area 111 112 o pagewidth Float pixel width of drawable area 113 114 o pageheight Float pixel height of drawable area 115 116 o xcenter Float X co-ord of center of drawable area 117 118 o ycenter Float Y co-ord of center of drawable area 119 120 o start Int, base to start drawing from 121 122 o end Int, base to stop drawing at 123 124 o length Size of sequence to be drawn 125 126 o track_size Float (0->1) the proportion of the track height to 127 draw in 128 129 o drawing Drawing canvas 130 131 o drawn_tracks List of ints denoting which tracks are to be drawn 132 133 o current_track_level Int denoting which track is currently being 134 drawn 135 136 o track_offsets Dictionary of number of pixels that each track top, 137 center and bottom is offset from the base of a 138 fragment, keyed by track 139 140 o sweep Float (0->1) the proportion of the circle circumference to 141 use for the diagram 142 143 o cross_track_links List of tuples each with four entries (track A, 144 feature A, track B, feature B) to be linked. 145 """
146 - def __init__(self, parent=None, pagesize='A3', orientation='landscape', 147 x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None, 148 start=None, end=None, tracklines=0, track_size=0.75, 149 circular=1, circle_core=0.0, cross_track_links=None):
150 """Create CircularDrawer object. 151 152 o parent Diagram object containing the data that the drawer 153 draws 154 155 o pagesize String describing the ISO size of the image, or a tuple 156 of pixels 157 158 o orientation String describing the required orientation of the 159 final drawing ('landscape' or 'portrait') 160 161 o x Float (0->1) describing the relative size of the X 162 margins to the page 163 164 o y Float (0->1) describing the relative size of the Y 165 margins to the page 166 167 o xl Float (0->1) describing the relative size of the left X 168 margin to the page (overrides x) 169 170 o xl Float (0->1) describing the relative size of the left X 171 margin to the page (overrides x) 172 173 o xr Float (0->1) describing the relative size of the right X 174 margin to the page (overrides x) 175 176 o yt Float (0->1) describing the relative size of the top Y 177 margin to the page (overrides y) 178 179 o yb Float (0->1) describing the relative size of the lower Y 180 margin to the page (overrides y) 181 182 o start Int, the position to begin drawing the diagram at 183 184 o end Int, the position to stop drawing the diagram at 185 186 o tracklines Boolean flag to show (or not) lines delineating tracks 187 on the diagram 188 189 o track_size The proportion of the available track height that 190 should be taken up in drawing 191 192 o circular Boolean flaw to show whether the passed sequence is 193 circular or not 194 195 o circle_core The proportion of the available radius to leave 196 empty at the center of a circular diagram (0 to 1). 197 198 o cross_track_links List of tuples each with four entries (track A, 199 feature A, track B, feature B) to be linked. 200 """ 201 # Use the superclass' instantiation method 202 AbstractDrawer.__init__(self, parent, pagesize, orientation, 203 x, y, xl, xr, yt, yb, start, end, 204 tracklines, cross_track_links) 205 206 # Useful measurements on the page 207 self.track_size = track_size 208 self.circle_core = circle_core 209 if not circular: # Determine the proportion of the circumference 210 self.sweep = 0.9 # around which information will be drawn 211 else: 212 self.sweep = 1
213
214 - def set_track_heights(self):
215 """Initialise track heights. 216 217 Since tracks may not be of identical heights, the bottom and top 218 radius for each track is stored in a dictionary - self.track_radii, 219 keyed by track number 220 """ 221 bot_track = min(min(self.drawn_tracks), 1) 222 top_track = max(self.drawn_tracks) # The 'highest' track to draw 223 224 trackunit_sum = 0 # Total number of 'units' taken up by all tracks 225 trackunits = {} # Start and & units for each track keyed by track number 226 heightholder = 0 # placeholder variable 227 for track in range(bot_track, top_track+1): # track numbers to 'draw' 228 try: 229 trackheight = self._parent[track].height # Get track height 230 except: 231 trackheight = 1 232 trackunit_sum += trackheight # increment total track unit height 233 trackunits[track] = (heightholder, heightholder+trackheight) 234 heightholder += trackheight # move to next height 235 236 max_radius = 0.5*min(self.pagewidth, self.pageheight) 237 trackunit_height = max_radius * (1 - self.circle_core) / trackunit_sum 238 track_core = max_radius * self.circle_core 239 240 # Calculate top and bottom radii for each track 241 self.track_radii = {} # The inner, outer and center radii for each track 242 track_crop = trackunit_height*(1-self.track_size)/2. # 'step back' in pixels 243 for track in trackunits: 244 top = trackunits[track][1]*trackunit_height - track_crop + track_core 245 btm = trackunits[track][0]*trackunit_height + track_crop + track_core 246 ctr = btm+(top-btm)/2. 247 self.track_radii[track] = (btm, ctr, top)
248
249 - def draw(self):
250 """Draw a circular diagram of the stored data.""" 251 # Instantiate the drawing canvas 252 self.drawing = Drawing(self.pagesize[0], self.pagesize[1]) 253 254 feature_elements = [] # holds feature elements 255 feature_labels = [] # holds feature labels 256 greytrack_bgs = [] # holds track background 257 greytrack_labels = [] # holds track foreground labels 258 scale_axes = [] # holds scale axes 259 scale_labels = [] # holds scale axis labels 260 261 # Get tracks to be drawn and set track sizes 262 self.drawn_tracks = self._parent.get_drawn_levels() 263 self.set_track_heights() 264 265 # Go through each track in the parent (if it is to be drawn) one by 266 # one and collate the data as drawing elements 267 for track_level in self._parent.get_drawn_levels(): 268 self.current_track_level = track_level 269 track = self._parent[track_level] 270 gbgs, glabels = self.draw_greytrack(track) # Greytracks 271 greytrack_bgs.append(gbgs) 272 greytrack_labels.append(glabels) 273 features, flabels = self.draw_track(track) # Features and graphs 274 feature_elements.append(features) 275 feature_labels.append(flabels) 276 if track.scale: 277 axes, slabels = self.draw_scale(track) # Scale axes 278 scale_axes.append(axes) 279 scale_labels.append(slabels) 280 281 feature_cross_links = [] 282 for cross_link_obj in self.cross_track_links: 283 cross_link_elements = self.draw_cross_link(cross_link_obj) 284 if cross_link_elements: 285 feature_cross_links.append(cross_link_elements) 286 287 # Groups listed in order of addition to page (from back to front) 288 # Draw track backgrounds 289 # Draw feature cross track links 290 # Draw features and graphs 291 # Draw scale axes 292 # Draw scale labels 293 # Draw feature labels 294 # Draw track labels 295 element_groups = [greytrack_bgs, feature_cross_links, 296 feature_elements, 297 scale_axes, scale_labels, 298 feature_labels, greytrack_labels 299 ] 300 for element_group in element_groups: 301 for element_list in element_group: 302 [self.drawing.add(element) for element in element_list] 303 304 if self.tracklines: 305 # Draw test tracks over top of diagram 306 self.draw_test_tracks()
307
308 - def draw_track(self, track):
309 """Returns list of track elements and list of track labels.""" 310 track_elements = [] # Holds elements for features and graphs 311 track_labels = [] # Holds labels for features and graphs 312 313 # Distribution dictionary for dealing with different set types 314 set_methods = {FeatureSet: self.draw_feature_set, 315 GraphSet: self.draw_graph_set 316 } 317 318 for set in track.get_sets(): # Draw the feature or graph sets 319 elements, labels = set_methods[set.__class__](set) 320 track_elements += elements 321 track_labels += labels 322 return track_elements, track_labels
323
324 - def draw_feature_set(self, set):
325 """Returns list of feature elements and list of labels for them.""" 326 #print 'draw feature set' 327 feature_elements = [] # Holds diagram elements belonging to the features 328 label_elements = [] # Holds diagram elements belonging to feature labels 329 330 # Collect all the elements for the feature set 331 for feature in set.get_features(): 332 if self.is_in_bounds(feature.start) or self.is_in_bounds(feature.end): 333 features, labels = self.draw_feature(feature) 334 feature_elements += features 335 label_elements += labels 336 337 return feature_elements, label_elements
338
339 - def draw_feature(self, feature):
340 """Returns list of feature elements and list of labels for them.""" 341 feature_elements = [] # Holds drawable elements for a single feature 342 label_elements = [] # Holds labels for a single feature 343 344 if feature.hide: # Don't show feature: return early 345 return feature_elements, label_elements 346 347 start, end = self._current_track_start_end() 348 # A single feature may be split into subfeatures, so loop over them 349 for locstart, locend in feature.locations: 350 if locend < start: 351 continue 352 locstart = max(locstart, start) 353 if end < locstart: 354 continue 355 locend = min(locend, end) 356 # Get sigil for the feature/ each subfeature 357 feature_sigil, label = self.get_feature_sigil(feature, locstart, locend) 358 feature_elements.append(feature_sigil) 359 if label is not None: # If there's a label 360 label_elements.append(label) 361 362 return feature_elements, label_elements
363
364 - def get_feature_sigil(self, feature, locstart, locend, **kwargs):
365 """Returns graphics for feature, and any required label for it. 366 367 o feature Feature object 368 369 o locstart The start position of the feature 370 371 o locend The end position of the feature 372 """ 373 # Establish the co-ordinates for the sigil 374 btm, ctr, top = self.track_radii[self.current_track_level] 375 376 startangle, startcos, startsin = self.canvas_angle(locstart) 377 endangle, endcos, endsin = self.canvas_angle(locend) 378 midangle, midcos, midsin = self.canvas_angle(float(locend+locstart)/2) 379 380 # Distribution dictionary for various ways of drawing the feature 381 # Each method takes the inner and outer radii, the start and end angle 382 # subtended at the diagram center, and the color as arguments 383 draw_methods = {'BOX': self._draw_sigil_box, 384 'OCTO': self._draw_sigil_cut_corner_box, 385 'JAGGY': self._draw_sigil_jaggy, 386 'ARROW': self._draw_sigil_arrow, 387 'BIGARROW': self._draw_sigil_big_arrow, 388 } 389 390 # Get sigil for the feature, location dependent on the feature strand 391 method = draw_methods[feature.sigil] 392 kwargs['head_length_ratio'] = feature.arrowhead_length 393 kwargs['shaft_height_ratio'] = feature.arrowshaft_height 394 395 #Support for clickable links... needs ReportLab 2.4 or later 396 #which added support for links in SVG output. 397 if hasattr(feature, "url") : 398 kwargs["hrefURL"] = feature.url 399 kwargs["hrefTitle"] = feature.name 400 401 sigil = method(btm, ctr, top, startangle, endangle, feature.strand, 402 color=feature.color, border=feature.border, **kwargs) 403 404 if feature.label: # Feature needs a label 405 label = String(0, 0, feature.name.strip(), 406 fontName=feature.label_font, 407 fontSize=feature.label_size, 408 fillColor=feature.label_color) 409 labelgroup = Group(label) 410 label_angle = startangle + 0.5 * pi # Make text radial 411 sinval, cosval = startsin, startcos 412 if feature.strand != -1: 413 # Feature is on top, or covers both strands 414 if startangle < pi: # Turn text round and anchor end to inner radius 415 sinval, cosval = endsin, endcos 416 label_angle = endangle - 0.5 * pi 417 labelgroup.contents[0].textAnchor = 'end' 418 pos = self.xcenter+top*sinval 419 coslabel = cos(label_angle) 420 sinlabel = sin(label_angle) 421 labelgroup.transform = (coslabel,-sinlabel,sinlabel,coslabel, 422 pos, self.ycenter+top*cosval) 423 else: 424 # Feature on bottom strand 425 if startangle < pi: # Turn text round and anchor end to inner radius 426 sinval, cosval = endsin, endcos 427 label_angle = endangle - 0.5 * pi 428 else: 429 labelgroup.contents[0].textAnchor = 'end' 430 pos = self.xcenter+btm*sinval 431 coslabel = cos(label_angle) 432 sinlabel = sin(label_angle) 433 labelgroup.transform = (coslabel,-sinlabel,sinlabel,coslabel, 434 pos, self.ycenter+btm*cosval) 435 436 else: 437 labelgroup = None 438 #if locstart > locend: 439 # print locstart, locend, feature.strand, sigil, feature.name 440 #print locstart, locend, feature.name 441 return sigil, labelgroup
442 516
517 - def draw_graph_set(self, set):
518 """Returns list of graph elements and list of their labels. 519 520 o set GraphSet object 521 """ 522 #print 'draw graph set' 523 elements = [] # Holds graph elements 524 525 # Distribution dictionary for how to draw the graph 526 style_methods = {'line': self.draw_line_graph, 527 'heat': self.draw_heat_graph, 528 'bar': self.draw_bar_graph 529 } 530 531 for graph in set.get_graphs(): 532 elements += style_methods[graph.style](graph) 533 534 return elements, []
535
536 - def draw_line_graph(self, graph):
537 """Returns line graph as list of drawable elements. 538 539 o graph GraphData object 540 """ 541 line_elements = [] # holds drawable elements 542 543 # Get graph data 544 data_quartiles = graph.quartiles() 545 minval, maxval = data_quartiles[0],data_quartiles[4] 546 btm, ctr, top = self.track_radii[self.current_track_level] 547 trackheight = 0.5*(top-btm) 548 datarange = maxval - minval 549 if datarange == 0: 550 datarange = trackheight 551 552 start, end = self._current_track_start_end() 553 data = graph[start:end] 554 555 if not data: 556 return [] 557 558 # midval is the value at which the x-axis is plotted, and is the 559 # central ring in the track 560 if graph.center is None: 561 midval = (maxval + minval)/2. 562 else: 563 midval = graph.center 564 # Whichever is the greatest difference: max-midval or min-midval, is 565 # taken to specify the number of pixel units resolved along the 566 # y-axis 567 resolution = max((midval-minval), (maxval-midval)) 568 569 # Start from first data point 570 pos, val = data[0] 571 lastangle, lastcos, lastsin = self.canvas_angle(pos) 572 # We calculate the track height 573 posheight = trackheight*(val-midval)/resolution + ctr 574 lastx = self.xcenter+posheight*lastsin # start xy coords 575 lasty = self.ycenter+posheight*lastcos 576 for pos, val in data: 577 posangle, poscos, possin = self.canvas_angle(pos) 578 posheight = trackheight*(val-midval)/resolution + ctr 579 x = self.xcenter+posheight*possin # next xy coords 580 y = self.ycenter+posheight*poscos 581 line_elements.append(Line(lastx, lasty, x, y, 582 strokeColor = graph.poscolor, 583 strokeWidth = graph.linewidth)) 584 lastx, lasty, = x, y 585 return line_elements
586
587 - def draw_bar_graph(self, graph):
588 """Returns list of drawable elements for a bar graph. 589 590 o graph Graph object 591 """ 592 # At each point contained in the graph data, we draw a vertical bar 593 # from the track center to the height of the datapoint value (positive 594 # values go up in one color, negative go down in the alternative 595 # color). 596 bar_elements = [] 597 598 # Set the number of pixels per unit for the data 599 data_quartiles = graph.quartiles() 600 minval, maxval = data_quartiles[0],data_quartiles[4] 601 btm, ctr, top = self.track_radii[self.current_track_level] 602 trackheight = 0.5*(top-btm) 603 datarange = maxval - minval 604 if datarange == 0: 605 datarange = trackheight 606 data = graph[self.start:self.end] 607 # midval is the value at which the x-axis is plotted, and is the 608 # central ring in the track 609 if graph.center is None: 610 midval = (maxval + minval)/2. 611 else: 612 midval = graph.center 613 614 # Convert data into 'binned' blocks, covering half the distance to the 615 # next data point on either side, accounting for the ends of fragments 616 # and tracks 617 start, end = self._current_track_start_end() 618 data = intermediate_points(start, end, graph[start:end]) 619 620 if not data: 621 return [] 622 623 # Whichever is the greatest difference: max-midval or min-midval, is 624 # taken to specify the number of pixel units resolved along the 625 # y-axis 626 resolution = max((midval-minval), (maxval-midval)) 627 if resolution == 0: 628 resolution = trackheight 629 630 # Create elements for the bar graph based on newdata 631 for pos0, pos1, val in data: 632 pos0angle, pos0cos, pos0sin = self.canvas_angle(pos0) 633 pos1angle, pos1cos, pos1sin = self.canvas_angle(pos1) 634 635 barval = trackheight*(val-midval)/resolution 636 if barval >=0: 637 barcolor = graph.poscolor 638 else: 639 barcolor = graph.negcolor 640 641 # Draw bar 642 bar_elements.append(self._draw_arc(ctr, ctr+barval, pos0angle, 643 pos1angle, barcolor)) 644 return bar_elements
645
646 - def draw_heat_graph(self, graph):
647 """Returns list of drawable elements for the heat graph. 648 649 o graph Graph object 650 """ 651 # At each point contained in the graph data, we draw a box that is the 652 # full height of the track, extending from the midpoint between the 653 # previous and current data points to the midpoint between the current 654 # and next data points 655 heat_elements = [] # holds drawable elements 656 657 # Get graph data 658 data_quartiles = graph.quartiles() 659 minval, maxval = data_quartiles[0],data_quartiles[4] 660 midval = (maxval + minval)/2. # mid is the value at the X-axis 661 btm, ctr, top = self.track_radii[self.current_track_level] 662 trackheight = (top-btm) 663 664 start, end = self._current_track_start_end() 665 data = intermediate_points(start, end, graph[start:end]) 666 667 # Create elements on the graph, indicating a large positive value by 668 # the graph's poscolor, and a large negative value by the graph's 669 # negcolor attributes 670 for pos0, pos1, val in data: 671 pos0angle, pos0cos, pos0sin = self.canvas_angle(pos0) 672 pos1angle, pos1cos, pos1sin = self.canvas_angle(pos1) 673 674 # Calculate the heat color, based on the differential between 675 # the value and the median value 676 heat = colors.linearlyInterpolatedColor(graph.poscolor, 677 graph.negcolor, 678 maxval, minval, val) 679 680 # Draw heat box 681 heat_elements.append(self._draw_arc(btm, top, pos0angle, pos1angle, 682 heat, border=heat)) 683 return heat_elements
684
685 - def draw_scale(self, track):
686 """Returns list of elements in the scale and list of their labels. 687 688 o track Track object 689 """ 690 scale_elements = [] # holds axes and ticks 691 scale_labels = [] # holds labels 692 693 if not track.scale: 694 # no scale required, exit early 695 return [], [] 696 697 # Get track locations 698 btm, ctr, top = self.track_radii[self.current_track_level] 699 trackheight = (top-ctr) 700 701 # X-axis 702 start, end = self._current_track_start_end() 703 if track.start is not None or track.end is not None: 704 # Draw an arc, leaving out the wedge 705 p = ArcPath(strokeColor=track.scale_color, fillColor=None) 706 startangle, startcos, startsin = self.canvas_angle(start) 707 endangle, endcos, endsin = self.canvas_angle(end) 708 p.addArc(self.xcenter, self.ycenter, ctr, 709 90 - (endangle * 180 / pi), 710 90 - (startangle * 180 / pi)) 711 scale_elements.append(p) 712 del p 713 # Y-axis start marker 714 x0, y0 = self.xcenter+btm*startsin, self.ycenter+btm*startcos 715 x1, y1 = self.xcenter+top*startsin, self.ycenter+top*startcos 716 scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) 717 # Y-axis end marker 718 x0, y0 = self.xcenter+btm*endsin, self.ycenter+btm*endcos 719 x1, y1 = self.xcenter+top*endsin, self.ycenter+top*endcos 720 scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) 721 elif self.sweep < 1: 722 # Draw an arc, leaving out the wedge 723 p = ArcPath(strokeColor=track.scale_color, fillColor=None) 724 # Note reportlab counts angles anti-clockwise from the horizontal 725 # (as in mathematics, e.g. complex numbers and polar coordinates) 726 # in degrees. 727 p.addArc(self.xcenter, self.ycenter, ctr, 728 startangledegrees=90-360*self.sweep, 729 endangledegrees=90) 730 scale_elements.append(p) 731 del p 732 # Y-axis start marker 733 x0, y0 = self.xcenter, self.ycenter+btm 734 x1, y1 = self.xcenter, self.ycenter+top 735 scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) 736 # Y-axis end marker 737 alpha = 2*pi*self.sweep 738 x0, y0 = self.xcenter+btm*sin(alpha), self.ycenter+btm*cos(alpha) 739 x1, y1 = self.xcenter+top*sin(alpha), self.ycenter+top*cos(alpha) 740 scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) 741 else: 742 # Draw a full circle 743 scale_elements.append(Circle(self.xcenter, self.ycenter, ctr, 744 strokeColor=track.scale_color, 745 fillColor=None)) 746 747 start, end = self._current_track_start_end() 748 if track.scale_ticks: # Ticks are required on the scale 749 # Draw large ticks 750 # I want the ticks to be consistently positioned relative to 751 # the start of the sequence (position 0), not relative to the 752 # current viewpoint (self.start and self.end) 753 754 ticklen = track.scale_largeticks * trackheight 755 tickiterval = int(track.scale_largetick_interval) 756 # Note that we could just start the list of ticks using 757 # range(0,self.end,tickinterval) and the filter out the 758 # ones before self.start - but this seems wasteful. 759 # Using tickiterval * (self.start/tickiterval) is a shortcut. 760 for tickpos in range(tickiterval * (self.start//tickiterval), 761 int(self.end), tickiterval): 762 if tickpos <= start or end <= tickpos: 763 continue 764 tick, label = self.draw_tick(tickpos, ctr, ticklen, 765 track, 766 track.scale_largetick_labels) 767 scale_elements.append(tick) 768 if label is not None: # If there's a label, add it 769 scale_labels.append(label) 770 # Draw small ticks 771 ticklen = track.scale_smallticks * trackheight 772 tickiterval = int(track.scale_smalltick_interval) 773 for tickpos in range(tickiterval * (self.start//tickiterval), 774 int(self.end), tickiterval): 775 if tickpos <= start or end <= tickpos: 776 continue 777 tick, label = self.draw_tick(tickpos, ctr, ticklen, 778 track, 779 track.scale_smalltick_labels) 780 scale_elements.append(tick) 781 if label is not None: # If there's a label, add it 782 scale_labels.append(label) 783 784 # Check to see if the track contains a graph - if it does, get the 785 # minimum and maximum values, and put them on the scale Y-axis 786 # at 60 degree intervals, ordering the labels by graph_id 787 startangle, startcos, startsin = self.canvas_angle(start) 788 endangle, endcos, endsin = self.canvas_angle(end) 789 if track.axis_labels: 790 for set in track.get_sets(): 791 if set.__class__ is GraphSet: 792 # Y-axis 793 for n in xrange(7): 794 angle = n * 1.0471975511965976 795 if angle < startangle or endangle < angle: 796 continue 797 ticksin, tickcos = sin(angle), cos(angle) 798 x0, y0 = self.xcenter+btm*ticksin, self.ycenter+btm*tickcos 799 x1, y1 = self.xcenter+top*ticksin, self.ycenter+top*tickcos 800 scale_elements.append(Line(x0, y0, x1, y1, 801 strokeColor=track.scale_color)) 802 803 graph_label_min = [] 804 graph_label_max = [] 805 graph_label_mid = [] 806 for graph in set.get_graphs(): 807 quartiles = graph.quartiles() 808 minval, maxval = quartiles[0], quartiles[4] 809 if graph.center is None: 810 midval = (maxval + minval)/2. 811 graph_label_min.append("%.3f" % minval) 812 graph_label_max.append("%.3f" % maxval) 813 graph_label_mid.append("%.3f" % midval) 814 else: 815 diff = max((graph.center-minval), 816 (maxval-graph.center)) 817 minval = graph.center-diff 818 maxval = graph.center+diff 819 midval = graph.center 820 graph_label_mid.append("%.3f" % midval) 821 graph_label_min.append("%.3f" % minval) 822 graph_label_max.append("%.3f" % maxval) 823 xmid, ymid = (x0+x1)/2., (y0+y1)/2. 824 for limit, x, y, in [(graph_label_min, x0, y0), 825 (graph_label_max, x1, y1), 826 (graph_label_mid, xmid, ymid)]: 827 label = String(0, 0, ";".join(limit), 828 fontName=track.scale_font, 829 fontSize=track.scale_fontsize, 830 fillColor=track.scale_color) 831 label.textAnchor = 'middle' 832 labelgroup = Group(label) 833 labelgroup.transform = (tickcos, -ticksin, 834 ticksin, tickcos, 835 x, y) 836 scale_labels.append(labelgroup) 837 838 return scale_elements, scale_labels
839
840 - def draw_tick(self, tickpos, ctr, ticklen, track, draw_label):
841 """Returns drawing element for a tick on the scale. 842 843 o tickpos Int, position of the tick on the sequence 844 845 o ctr Float, Y co-ord of the center of the track 846 847 o ticklen How long to draw the tick 848 849 o track Track, the track the tick is drawn on 850 851 o draw_label Boolean, write the tick label? 852 """ 853 # Calculate tick co-ordinates 854 tickangle, tickcos, ticksin = self.canvas_angle(tickpos) 855 x0, y0 = self.xcenter+ctr*ticksin, self.ycenter+ctr*tickcos 856 x1, y1 = self.xcenter+(ctr+ticklen)*ticksin, self.ycenter+(ctr+ticklen)*tickcos 857 # Calculate height of text label so it can be offset on lower half 858 # of diagram 859 # LP: not used, as not all fonts have ascent_descent data in reportlab.pdfbase._fontdata 860 #label_offset = _fontdata.ascent_descent[track.scale_font][0]*\ 861 # track.scale_fontsize/1000. 862 tick = Line(x0, y0, x1, y1, strokeColor=track.scale_color) 863 if draw_label: 864 # Put tick position on as label 865 if track.scale_format == 'SInt': 866 if tickpos >= 1000000: 867 tickstring = str(tickpos//1000000) + " Mbp" 868 elif tickpos >= 1000: 869 tickstring = str(tickpos//1000) + " Kbp" 870 else: 871 tickstring = str(tickpos) 872 else: 873 tickstring = str(tickpos) 874 label = String(0, 0, tickstring, # Make label string 875 fontName=track.scale_font, 876 fontSize=track.scale_fontsize, 877 fillColor=track.scale_color) 878 if tickangle > pi: 879 label.textAnchor = 'end' 880 # LP: This label_offset depends on ascent_descent data, which is not available for all 881 # fonts, so has been deprecated. 882 #if 0.5*pi < tickangle < 1.5*pi: 883 # y1 -= label_offset 884 labelgroup = Group(label) 885 labelgroup.transform = (1,0,0,1, x1, y1) 886 else: 887 labelgroup = None 888 return tick, labelgroup
889
890 - def draw_test_tracks(self):
891 """Draw blue test tracks with grene line down their center.""" 892 # Add lines only for drawn tracks 893 for track in self.drawn_tracks: 894 btm, ctr, top = self.track_radii[track] 895 self.drawing.add(Circle(self.xcenter, self.ycenter, top, 896 strokeColor=colors.blue, 897 fillColor=None)) # top line 898 self.drawing.add(Circle(self.xcenter, self.ycenter, ctr, 899 strokeColor=colors.green, 900 fillColor=None)) # middle line 901 self.drawing.add(Circle(self.xcenter, self.ycenter, btm, 902 strokeColor=colors.blue, 903 fillColor=None)) # bottom line
904
905 - def draw_greytrack(self, track):
906 """Drawing element for grey background to passed track. 907 908 o track Track object 909 """ 910 greytrack_bgs = [] # Holds track backgrounds 911 greytrack_labels = [] # Holds track foreground labels 912 913 if not track.greytrack: # No greytrack required, return early 914 return [], [] 915 916 # Get track location 917 btm, ctr, top = self.track_radii[self.current_track_level] 918 919 start, end = self._current_track_start_end() 920 startangle, startcos, startsin = self.canvas_angle(start) 921 endangle, endcos, endsin = self.canvas_angle(end) 922 923 # Make background 924 if track.start is not None or track.end is not None: 925 # Draw an arc, leaving out the wedge 926 p = ArcPath(strokeColor=track.scale_color, fillColor=None) 927 greytrack_bgs.append(self._draw_arc(btm, top, startangle, endangle, 928 colors.Color(0.96, 0.96, 0.96))) 929 elif self.sweep < 1: 930 # Make a partial circle, a large arc box 931 # This method assumes the correct center for us. 932 greytrack_bgs.append(self._draw_arc(btm, top, 0, 2*pi*self.sweep, 933 colors.Color(0.96, 0.96, 0.96))) 934 else: 935 # Make a full circle (using a VERY thick linewidth) 936 greytrack_bgs.append(Circle(self.xcenter, self.ycenter, ctr, 937 strokeColor = colors.Color(0.96, 0.96, 0.96), 938 fillColor=None, strokeWidth=top-btm)) 939 940 if track.greytrack_labels: 941 # Labels are required for this track 942 labelstep = self.length//track.greytrack_labels # label interval 943 for pos in range(self.start, self.end, labelstep): 944 label = String(0, 0, track.name, # Add a new label at 945 fontName=track.greytrack_font, # each interval 946 fontSize=track.greytrack_fontsize, 947 fillColor=track.greytrack_fontcolor) 948 theta, costheta, sintheta = self.canvas_angle(pos) 949 if theta < startangle or endangle < theta: 950 continue 951 x,y = self.xcenter+btm*sintheta, self.ycenter+btm*costheta # start text halfway up marker 952 labelgroup = Group(label) 953 labelangle = self.sweep*2*pi*(pos-self.start)/self.length - pi/2 954 if theta > pi: 955 label.textAnchor = 'end' # Anchor end of text to inner radius 956 labelangle += pi # and reorient it 957 cosA, sinA = cos(labelangle), sin(labelangle) 958 labelgroup.transform = (cosA, -sinA, sinA, 959 cosA, x, y) 960 if not self.length-x <= labelstep: # Don't overrun the circle 961 greytrack_labels.append(labelgroup) 962 963 return greytrack_bgs, greytrack_labels
964
965 - def canvas_angle(self, base):
966 """Given base-pair position, return (angle, cosine, sin).""" 967 angle = self.sweep*2*pi*(base-self.start)/self.length 968 return (angle, cos(angle), sin(angle))
969
970 - def _draw_sigil_box(self, bottom, center, top, 971 startangle, endangle, strand, 972 **kwargs):
973 """Draw BOX sigil.""" 974 if strand == 1: 975 inner_radius = center 976 outer_radius = top 977 elif strand == -1: 978 inner_radius = bottom 979 outer_radius = center 980 else: 981 inner_radius = bottom 982 outer_radius = top 983 return self._draw_arc(inner_radius, outer_radius, startangle, endangle, **kwargs)
984
985 - def _draw_arc(self, inner_radius, outer_radius, startangle, endangle, 986 color, border=None, colour=None, **kwargs):
987 """Returns close path describing an arc box. 988 989 o inner_radius Float distance of inside of arc from drawing center 990 991 o outer_radius Float distance of outside of arc from drawing center 992 993 o startangle Float angle subtended by start of arc at drawing center 994 (in radians) 995 996 o endangle Float angle subtended by end of arc at drawing center 997 (in radians) 998 999 o color colors.Color object for arc (overridden by backwards 1000 compatible argument with UK spelling, colour). 1001 1002 Returns a closed path object describing an arced box corresponding to 1003 the passed values. For very small angles, a simple four sided 1004 polygon is used. 1005 """ 1006 # Let the UK spelling (colour) override the USA spelling (color) 1007 if colour is not None: 1008 color = colour 1009 1010 strokecolor, color = _stroke_and_fill_colors(color, border) 1011 1012 if abs(float(endangle - startangle))>.01: 1013 # Wide arc, must use full curves 1014 p = ArcPath(strokeColor=strokecolor, 1015 fillColor=color, 1016 strokewidth=0) 1017 # Note reportlab counts angles anti-clockwise from the horizontal 1018 # (as in mathematics, e.g. complex numbers and polar coordinates) 1019 # but we use clockwise from the vertical. Also reportlab uses 1020 # degrees, but we use radians. 1021 p.addArc(self.xcenter, self.ycenter, inner_radius, 1022 90 - (endangle * 180 / pi), 90 - (startangle * 180 / pi), 1023 moveTo=True) 1024 p.addArc(self.xcenter, self.ycenter, outer_radius, 1025 90 - (endangle * 180 / pi), 90 - (startangle * 180 / pi), 1026 reverse=True) 1027 p.closePath() 1028 return p 1029 else: 1030 # Cheat and just use a four sided polygon. 1031 # Calculate trig values for angle and coordinates 1032 startcos, startsin = cos(startangle), sin(startangle) 1033 endcos, endsin = cos(endangle), sin(endangle) 1034 x0,y0 = self.xcenter, self.ycenter # origin of the circle 1035 x1,y1 = (x0+inner_radius*startsin, y0+inner_radius*startcos) 1036 x2,y2 = (x0+inner_radius*endsin, y0+inner_radius*endcos) 1037 x3,y3 = (x0+outer_radius*endsin, y0+outer_radius*endcos) 1038 x4,y4 = (x0+outer_radius*startsin, y0+outer_radius*startcos) 1039 return draw_polygon([(x1,y1),(x2,y2),(x3,y3),(x4,y4)], color, border)
1040
1041 - def _draw_arc_line(self, path, start_radius, end_radius, start_angle, end_angle, 1042 move=False):
1043 """Adds a list of points to a path object. 1044 1045 Assumes angles given are in degrees! 1046 1047 Represents what would be a straight line on a linear diagram. 1048 """ 1049 x0, y0 = self.xcenter, self.ycenter # origin of the circle 1050 radius_diff = end_radius - start_radius 1051 angle_diff = end_angle - start_angle 1052 dx = 0.01 # heuristic 1053 a = start_angle*pi/180 1054 if move: 1055 path.moveTo(x0+start_radius*cos(a), y0+start_radius*sin(a)) 1056 else: 1057 path.lineTo(x0+start_radius*cos(a), y0+start_radius*sin(a)) 1058 x = dx 1059 if 0.01 <= abs(dx): 1060 while x < 1: 1061 r = start_radius + x*radius_diff 1062 a = (start_angle + x * (angle_diff)) * pi / 180 # to radians for sin/cos 1063 #print x0+r*cos(a), y0+r*sin(a) 1064 path.lineTo(x0+r*cos(a), y0+r*sin(a)) 1065 x += dx 1066 a = end_angle*pi/180 1067 path.lineTo(x0+end_radius*cos(a), y0+end_radius*sin(a))
1068
1069 - def _draw_arc_poly(self, inner_radius, outer_radius, 1070 inner_startangle, inner_endangle, 1071 outer_startangle, outer_endangle, 1072 color, border=None, flip=False, 1073 **kwargs):
1074 """Returns polygon path describing an arc.""" 1075 strokecolor, color = _stroke_and_fill_colors(color, border) 1076 1077 x0, y0 = self.xcenter, self.ycenter # origin of the circle 1078 if abs(inner_endangle - outer_startangle)>0.01 \ 1079 or abs(outer_endangle - inner_startangle)>0.01 \ 1080 or abs(inner_startangle - outer_startangle)>0.01 \ 1081 or abs(outer_startangle - outer_startangle)>0.01: 1082 # Wide arc, must use full curves 1083 p = ArcPath(strokeColor=strokecolor, 1084 fillColor=color, 1085 #default is mitre/miter which can stick out too much: 1086 strokeLineJoin=1, # 1=round 1087 strokewidth=0) 1088 #Note reportlab counts angles anti-clockwise from the horizontal 1089 #(as in mathematics, e.g. complex numbers and polar coordinates) 1090 #but we use clockwise from the vertical. Also reportlab uses 1091 #degrees, but we use radians. 1092 i_start = 90 - (inner_startangle * 180 / pi) 1093 i_end = 90 - (inner_endangle * 180 / pi) 1094 o_start = 90 - (outer_startangle * 180 / pi) 1095 o_end = 90 - (outer_endangle * 180 / pi) 1096 p.addArc(x0, y0, inner_radius, i_end, i_start, 1097 moveTo=True, reverse=True) 1098 if flip: 1099 #Flipped, join end to start, 1100 self._draw_arc_line(p, inner_radius, outer_radius, i_end, o_start) 1101 p.addArc(x0, y0, outer_radius, o_end, o_start, reverse=True) 1102 self._draw_arc_line(p, outer_radius, inner_radius, o_end, i_start) 1103 else: 1104 #Not flipped, join start to start, end to end 1105 self._draw_arc_line(p, inner_radius, outer_radius, i_end, o_end) 1106 p.addArc(x0, y0, outer_radius, o_end, o_start, 1107 reverse=False) 1108 self._draw_arc_line(p, outer_radius, inner_radius, o_start, i_start) 1109 p.closePath() 1110 return p 1111 else: 1112 #Cheat and just use a four sided polygon. 1113 # Calculate trig values for angle and coordinates 1114 inner_startcos, inner_startsin = cos(inner_startangle), sin(inner_startangle) 1115 inner_endcos, inner_endsin = cos(inner_endangle), sin(inner_endangle) 1116 outer_startcos, outer_startsin = cos(outer_startangle), sin(outer_startangle) 1117 outer_endcos, outer_endsin = cos(outer_endangle), sin(outer_endangle) 1118 x1,y1 = (x0+inner_radius*inner_startsin, y0+inner_radius*inner_startcos) 1119 x2,y2 = (x0+inner_radius*inner_endsin, y0+inner_radius*inner_endcos) 1120 x3,y3 = (x0+outer_radius*outer_endsin, y0+outer_radius*outer_endcos) 1121 x4,y4 = (x0+outer_radius*outer_startsin, y0+outer_radius*outer_startcos) 1122 return draw_polygon([(x1,y1),(x2,y2),(x3,y3),(x4,y4)], color, border, 1123 #default is mitre/miter which can stick out too much: 1124 strokeLineJoin=1, # 1=round 1125 )
1126
1127 - def _draw_sigil_cut_corner_box(self, bottom, center, top, 1128 startangle, endangle, strand, 1129 color, border=None, corner=0.5, 1130 **kwargs):
1131 """Draw OCTO sigil, box with corners cut off.""" 1132 if strand == 1: 1133 inner_radius = center 1134 outer_radius = top 1135 elif strand == -1: 1136 inner_radius = bottom 1137 outer_radius = center 1138 else: 1139 inner_radius = bottom 1140 outer_radius = top 1141 1142 strokecolor, color = _stroke_and_fill_colors(color, border) 1143 1144 startangle, endangle = min(startangle, endangle), max(startangle, endangle) 1145 angle = float(endangle - startangle) 1146 1147 middle_radius = 0.5*(inner_radius+outer_radius) 1148 boxheight = outer_radius - inner_radius 1149 1150 corner_len = min(0.5*boxheight, 0.5*boxheight*corner) 1151 shaft_inner_radius = inner_radius + corner_len 1152 shaft_outer_radius = outer_radius - corner_len 1153 1154 cornerangle_delta = max(0.0,min(abs(boxheight)*0.5*corner/middle_radius, abs(angle*0.5))) 1155 if angle < 0: 1156 cornerangle_delta *= -1 # reverse it 1157 1158 # Calculate trig values for angle and coordinates 1159 startcos, startsin = cos(startangle), sin(startangle) 1160 endcos, endsin = cos(endangle), sin(endangle) 1161 x0, y0 = self.xcenter, self.ycenter # origin of the circle 1162 p = ArcPath(strokeColor=strokecolor, 1163 fillColor=color, 1164 strokeLineJoin=1, # 1=round 1165 strokewidth=0, 1166 **kwargs) 1167 #Inner curved edge 1168 p.addArc(self.xcenter, self.ycenter, inner_radius, 1169 90 - ((endangle-cornerangle_delta) * 180 / pi), 1170 90 - ((startangle+cornerangle_delta) * 180 / pi), 1171 moveTo=True) 1172 #Corner edge - straight lines assumes small angle! 1173 #TODO - Use self._draw_arc_line(p, ...) here if we expose corner setting 1174 p.lineTo(x0+shaft_inner_radius*startsin, y0+shaft_inner_radius*startcos) 1175 p.lineTo(x0+shaft_outer_radius*startsin, y0+shaft_outer_radius*startcos) 1176 #Outer curved edge 1177 p.addArc(self.xcenter, self.ycenter, outer_radius, 1178 90 - ((endangle-cornerangle_delta) * 180 / pi), 1179 90 - ((startangle+cornerangle_delta) * 180 / pi), 1180 reverse=True) 1181 #Corner edges 1182 p.lineTo(x0+shaft_outer_radius*endsin, y0+shaft_outer_radius*endcos) 1183 p.lineTo(x0+shaft_inner_radius*endsin, y0+shaft_inner_radius*endcos) 1184 p.closePath() 1185 return p
1186
1187 - def _draw_sigil_arrow(self, bottom, center, top, 1188 startangle, endangle, strand, 1189 **kwargs):
1190 """Draw ARROW sigil.""" 1191 if strand == 1: 1192 inner_radius = center 1193 outer_radius = top 1194 orientation = "right" 1195 elif strand == -1: 1196 inner_radius = bottom 1197 outer_radius = center 1198 orientation = "left" 1199 else: 1200 inner_radius = bottom 1201 outer_radius = top 1202 orientation = "right" # backwards compatibility 1203 return self._draw_arc_arrow(inner_radius, outer_radius, startangle, endangle, 1204 orientation=orientation, **kwargs)
1205
1206 - def _draw_sigil_big_arrow(self, bottom, center, top, 1207 startangle, endangle, strand, 1208 **kwargs):
1209 """Draw BIGARROW sigil, like ARROW but straddles the axis.""" 1210 if strand == -1: 1211 orientation = "left" 1212 else: 1213 orientation = "right" 1214 return self._draw_arc_arrow(bottom, top, startangle, endangle, 1215 orientation=orientation, **kwargs)
1216
1217 - def _draw_arc_arrow(self, inner_radius, outer_radius, startangle, endangle, 1218 color, border=None, 1219 shaft_height_ratio=0.4, head_length_ratio=0.5, orientation='right', 1220 colour=None, **kwargs):
1221 """Draw an arrow along an arc.""" 1222 #Let the UK spelling (colour) override the USA spelling (color) 1223 if colour is not None: 1224 color = colour 1225 1226 strokecolor, color = _stroke_and_fill_colors(color, border) 1227 1228 #if orientation == 'right': 1229 # startangle, endangle = min(startangle, endangle), max(startangle, endangle) 1230 #elif orientation == 'left': 1231 # startangle, endangle = max(startangle, endangle), min(startangle, endangle) 1232 #else: 1233 startangle, endangle = min(startangle, endangle), max(startangle, endangle) 1234 if orientation != "left" and orientation != "right": 1235 raise ValueError("Invalid orientation %s, should be 'left' or 'right'" 1236 % repr(orientation)) 1237 1238 angle = float(endangle - startangle) # angle subtended by arc 1239 middle_radius = 0.5*(inner_radius+outer_radius) 1240 boxheight = outer_radius - inner_radius 1241 shaft_height = boxheight*shaft_height_ratio 1242 shaft_inner_radius = middle_radius - 0.5*shaft_height 1243 shaft_outer_radius = middle_radius + 0.5*shaft_height 1244 headangle_delta = max(0.0,min(abs(boxheight)*head_length_ratio/middle_radius, abs(angle))) 1245 if angle < 0: 1246 headangle_delta *= -1 # reverse it 1247 if orientation=="right": 1248 headangle = endangle-headangle_delta 1249 else: 1250 headangle = startangle+headangle_delta 1251 if startangle <= endangle: 1252 headangle = max(min(headangle, endangle), startangle) 1253 else: 1254 headangle = max(min(headangle, startangle), endangle) 1255 assert startangle <= headangle <= endangle \ 1256 or endangle <= headangle <= startangle, \ 1257 (startangle, headangle, endangle, angle) 1258 1259 # Calculate trig values for angle and coordinates 1260 startcos, startsin = cos(startangle), sin(startangle) 1261 headcos, headsin = cos(headangle), sin(headangle) 1262 endcos, endsin = cos(endangle), sin(endangle) 1263 x0,y0 = self.xcenter, self.ycenter # origin of the circle 1264 if 0.5 >= abs(angle) and abs(headangle_delta) >= abs(angle): 1265 #If the angle is small, and the arrow is all head, 1266 #cheat and just use a triangle. 1267 if orientation=="right": 1268 x1,y1 = (x0+inner_radius*startsin, y0+inner_radius*startcos) 1269 x2,y2 = (x0+outer_radius*startsin, y0+outer_radius*startcos) 1270 x3,y3 = (x0+middle_radius*endsin, y0+middle_radius*endcos) 1271 else: 1272 x1,y1 = (x0+inner_radius*endsin, y0+inner_radius*endcos) 1273 x2,y2 = (x0+outer_radius*endsin, y0+outer_radius*endcos) 1274 x3,y3 = (x0+middle_radius*startsin, y0+middle_radius*startcos) 1275 #return draw_polygon([(x1,y1),(x2,y2),(x3,y3)], color, border, 1276 # stroke_line_join=1) 1277 return Polygon([x1,y1,x2,y2,x3,y3], 1278 strokeColor=border or color, 1279 fillColor=color, 1280 strokeLineJoin=1, # 1=round, not mitre! 1281 strokewidth=0) 1282 elif orientation=="right": 1283 p = ArcPath(strokeColor=strokecolor, 1284 fillColor=color, 1285 #default is mitre/miter which can stick out too much: 1286 strokeLineJoin=1, # 1=round 1287 strokewidth=0, 1288 **kwargs) 1289 #Note reportlab counts angles anti-clockwise from the horizontal 1290 #(as in mathematics, e.g. complex numbers and polar coordinates) 1291 #but we use clockwise from the vertical. Also reportlab uses 1292 #degrees, but we use radians. 1293 p.addArc(self.xcenter, self.ycenter, shaft_inner_radius, 1294 90 - (headangle * 180 / pi), 90 - (startangle * 180 / pi), 1295 moveTo=True) 1296 p.addArc(self.xcenter, self.ycenter, shaft_outer_radius, 1297 90 - (headangle * 180 / pi), 90 - (startangle * 180 / pi), 1298 reverse=True) 1299 if abs(angle) < 0.5: 1300 p.lineTo(x0+outer_radius*headsin, y0+outer_radius*headcos) 1301 p.lineTo(x0+middle_radius*endsin, y0+middle_radius*endcos) 1302 p.lineTo(x0+inner_radius*headsin, y0+inner_radius*headcos) 1303 else: 1304 self._draw_arc_line(p, outer_radius, middle_radius, 1305 90 - (headangle * 180 / pi), 1306 90 - (endangle * 180 / pi)) 1307 self._draw_arc_line(p, middle_radius, inner_radius, 1308 90 - (endangle * 180 / pi), 1309 90 - (headangle * 180 / pi)) 1310 p.closePath() 1311 return p 1312 else: 1313 p = ArcPath(strokeColor=strokecolor, 1314 fillColor=color, 1315 #default is mitre/miter which can stick out too much: 1316 strokeLineJoin=1, # 1=round 1317 strokewidth=0, 1318 **kwargs) 1319 #Note reportlab counts angles anti-clockwise from the horizontal 1320 #(as in mathematics, e.g. complex numbers and polar coordinates) 1321 #but we use clockwise from the vertical. Also reportlab uses 1322 #degrees, but we use radians. 1323 p.addArc(self.xcenter, self.ycenter, shaft_inner_radius, 1324 90 - (endangle * 180 / pi), 90 - (headangle * 180 / pi), 1325 moveTo=True, reverse=True) 1326 p.addArc(self.xcenter, self.ycenter, shaft_outer_radius, 1327 90 - (endangle * 180 / pi), 90 - (headangle * 180 / pi), 1328 reverse=False) 1329 #Note - two staight lines is only a good approximation for small 1330 #head angle, in general will need to curved lines here: 1331 if abs(angle) < 0.5: 1332 p.lineTo(x0+outer_radius*headsin, y0+outer_radius*headcos) 1333 p.lineTo(x0+middle_radius*startsin, y0+middle_radius*startcos) 1334 p.lineTo(x0+inner_radius*headsin, y0+inner_radius*headcos) 1335 else: 1336 self._draw_arc_line(p, outer_radius, middle_radius, 1337 90 - (headangle * 180 / pi), 1338 90 - (startangle * 180 / pi)) 1339 self._draw_arc_line(p, middle_radius, inner_radius, 1340 90 - (startangle * 180 / pi), 1341 90 - (headangle * 180 / pi)) 1342 p.closePath() 1343 return p
1344
1345 - def _draw_sigil_jaggy(self, bottom, center, top, 1346 startangle, endangle, strand, 1347 color, border=None, 1348 **kwargs):
1349 """Draw JAGGY sigil. 1350 1351 Although we may in future expose the head/tail jaggy lengths, for now 1352 both the left and right edges are drawn jagged. 1353 """ 1354 if strand == 1: 1355 inner_radius = center 1356 outer_radius = top 1357 teeth = 2 1358 elif strand == -1: 1359 inner_radius = bottom 1360 outer_radius = center 1361 teeth = 2 1362 else: 1363 inner_radius = bottom 1364 outer_radius = top 1365 teeth = 4 1366 1367 #TODO, expose these settings? 1368 tail_length_ratio = 1.0 1369 head_length_ratio = 1.0 1370 1371 strokecolor, color = _stroke_and_fill_colors(color, border) 1372 1373 startangle, endangle = min(startangle, endangle), max(startangle, endangle) 1374 angle = float(endangle - startangle) # angle subtended by arc 1375 height = outer_radius - inner_radius 1376 1377 assert startangle <= endangle and angle >= 0 1378 if head_length_ratio and tail_length_ratio: 1379 headangle = max(endangle - min(height*head_length_ratio/(center*teeth), angle*0.5), startangle) 1380 tailangle = min(startangle + min(height*tail_length_ratio/(center*teeth), angle*0.5), endangle) 1381 elif head_length_ratio: 1382 headangle = max(endangle - min(height*head_length_ratio/(center*teeth), angle), startangle) 1383 tailangle = startangle 1384 else: 1385 headangle = endangle 1386 tailangle = min(startangle + min(height*tail_length_ratio/(center*teeth), angle), endangle) 1387 1388 assert startangle <= tailangle <= headangle <= endangle, \ 1389 (startangle, tailangle, headangle, endangle, angle) 1390 1391 # Calculate trig values for angle and coordinates 1392 startcos, startsin = cos(startangle), sin(startangle) 1393 headcos, headsin = cos(headangle), sin(headangle) 1394 endcos, endsin = cos(endangle), sin(endangle) 1395 x0,y0 = self.xcenter, self.ycenter # origin of the circle 1396 1397 p = ArcPath(strokeColor=strokecolor, 1398 fillColor=color, 1399 #default is mitre/miter which can stick out too much: 1400 strokeLineJoin=1, # 1=round 1401 strokewidth=0, 1402 **kwargs) 1403 #Note reportlab counts angles anti-clockwise from the horizontal 1404 #(as in mathematics, e.g. complex numbers and polar coordinates) 1405 #but we use clockwise from the vertical. Also reportlab uses 1406 #degrees, but we use radians. 1407 p.addArc(self.xcenter, self.ycenter, inner_radius, 1408 90 - (headangle * 180 / pi), 90 - (tailangle * 180 / pi), 1409 moveTo=True) 1410 for i in range(0, teeth): 1411 p.addArc(self.xcenter, self.ycenter, inner_radius+i*height/teeth, 1412 90 - (tailangle * 180 / pi), 90 - (startangle * 180 / pi)) 1413 #Curved line needed when drawing long jaggies 1414 self._draw_arc_line(p, 1415 inner_radius+i*height/teeth, 1416 inner_radius+(i+1)*height/teeth, 1417 90 - (startangle * 180 / pi), 1418 90 - (tailangle * 180 / pi)) 1419 p.addArc(self.xcenter, self.ycenter, outer_radius, 1420 90 - (headangle * 180 / pi), 90 - (tailangle * 180 / pi), 1421 reverse=True) 1422 for i in range(0, teeth): 1423 p.addArc(self.xcenter, self.ycenter, outer_radius-i*height/teeth, 1424 90 - (endangle * 180 / pi), 90 - (headangle * 180 / pi), 1425 reverse=True) 1426 #Curved line needed when drawing long jaggies 1427 self._draw_arc_line(p, 1428 outer_radius-i*height/teeth, 1429 outer_radius-(i+1)*height/teeth, 1430 90 - (endangle * 180 / pi), 1431 90 - (headangle * 180 / pi)) 1432 p.closePath() 1433 return p
1434