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