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