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