# Copyright 2003-2008 by Leighton Pritchard. All rights reserved. # Revisions copyright 2008-2017 by Peter Cock. # # This file is part of the Biopython distribution and governed by your # choice of the "Biopython License Agreement" or the "BSD 3-Clause License". # Please see the LICENSE file that should have been included as part of this # package. # # Contact: Leighton Pritchard, The James Hutton Institute, # Invergowrie, Dundee, Scotland, DD2 5DA, UK # Leighton.Pritchard@hutton.ac.uk ################################################################################ """CircularDrawer module for GenomeDiagram.""" # ReportLab imports from __future__ import print_function from reportlab.graphics.shapes import Drawing, String, Group, Line, Circle, Polygon from reportlab.lib import colors from reportlab.graphics.shapes import ArcPath from Bio._py3k import range # GenomeDiagram imports from ._AbstractDrawer import AbstractDrawer, draw_polygon, intermediate_points from ._AbstractDrawer import _stroke_and_fill_colors from ._FeatureSet import FeatureSet from ._GraphSet import GraphSet from math import pi, cos, sin class CircularDrawer(AbstractDrawer): """Object for drawing circular diagrams. Attributes: - tracklines Boolean for whether to draw lines dilineating tracks - pagesize Tuple describing the size of the page in pixels - x0 Float X co-ord for leftmost point of drawable area - xlim Float X co-ord for rightmost point of drawable area - y0 Float Y co-ord for lowest point of drawable area - ylim Float Y co-ord for topmost point of drawable area - pagewidth Float pixel width of drawable area - pageheight Float pixel height of drawable area - xcenter Float X co-ord of center of drawable area - ycenter Float Y co-ord of center of drawable area - start Int, base to start drawing from - end Int, base to stop drawing at - length Size of sequence to be drawn - track_size Float (0->1) the proportion of the track height to draw in - drawing Drawing canvas - drawn_tracks List of ints denoting which tracks are to be drawn - current_track_level Int denoting which track is currently being drawn - track_offsets Dictionary of number of pixels that each track top, center and bottom is offset from the base of a fragment, keyed by track - sweep Float (0->1) the proportion of the circle circumference to use for the diagram - cross_track_links List of tuples each with four entries (track A, feature A, track B, feature B) to be linked. """ def __init__(self, parent=None, pagesize='A3', orientation='landscape', x=0.05, y=0.05, xl=None, xr=None, yt=None, yb=None, start=None, end=None, tracklines=0, track_size=0.75, circular=1, circle_core=0.0, cross_track_links=None): """Create CircularDrawer object. Arguments: - parent Diagram object containing the data that the drawer draws - pagesize String describing the ISO size of the image, or a tuple of pixels - orientation String describing the required orientation of the final drawing ('landscape' or 'portrait') - x Float (0->1) describing the relative size of the X margins to the page - y Float (0->1) describing the relative size of the Y margins to the page - xl Float (0->1) describing the relative size of the left X margin to the page (overrides x) - xl Float (0->1) describing the relative size of the left X margin to the page (overrides x) - xr Float (0->1) describing the relative size of the right X margin to the page (overrides x) - yt Float (0->1) describing the relative size of the top Y margin to the page (overrides y) - yb Float (0->1) describing the relative size of the lower Y margin to the page (overrides y) - start Int, the position to begin drawing the diagram at - end Int, the position to stop drawing the diagram at - tracklines Boolean flag to show (or not) lines delineating tracks on the diagram - track_size The proportion of the available track height that should be taken up in drawing - circular Boolean flaw to show whether the passed sequence is circular or not - circle_core The proportion of the available radius to leave empty at the center of a circular diagram (0 to 1). - cross_track_links List of tuples each with four entries (track A, feature A, track B, feature B) to be linked. """ # Use the superclass' instantiation method AbstractDrawer.__init__(self, parent, pagesize, orientation, x, y, xl, xr, yt, yb, start, end, tracklines, cross_track_links) # Useful measurements on the page self.track_size = track_size self.circle_core = circle_core if not circular: # Determine the proportion of the circumference self.sweep = 0.9 # around which information will be drawn else: self.sweep = 1 def set_track_heights(self): """Initialize track heights. Since tracks may not be of identical heights, the bottom and top radius for each track is stored in a dictionary - self.track_radii, keyed by track number """ bot_track = min(min(self.drawn_tracks), 1) top_track = max(self.drawn_tracks) # The 'highest' track to draw trackunit_sum = 0 # Total number of 'units' taken up by all tracks trackunits = {} # Start and & units for each track keyed by track number heightholder = 0 # placeholder variable for track in range(bot_track, top_track + 1): # track numbers to 'draw' try: trackheight = self._parent[track].height # Get track height except Exception: # TODO: ValueError? IndexError? trackheight = 1 trackunit_sum += trackheight # increment total track unit height trackunits[track] = (heightholder, heightholder + trackheight) heightholder += trackheight # move to next height max_radius = 0.5 * min(self.pagewidth, self.pageheight) trackunit_height = max_radius * (1 - self.circle_core) / trackunit_sum track_core = max_radius * self.circle_core # Calculate top and bottom radii for each track self.track_radii = {} # The inner, outer and center radii for each track track_crop = trackunit_height * (1 - self.track_size) / 2. # 'step back' in pixels for track in trackunits: top = trackunits[track][1] * trackunit_height - track_crop + track_core btm = trackunits[track][0] * trackunit_height + track_crop + track_core ctr = btm + (top - btm) / 2. self.track_radii[track] = (btm, ctr, top) def draw(self): """Draw a circular diagram of the stored data.""" # Instantiate the drawing canvas self.drawing = Drawing(self.pagesize[0], self.pagesize[1]) feature_elements = [] # holds feature elements feature_labels = [] # holds feature labels greytrack_bgs = [] # holds track background greytrack_labels = [] # holds track foreground labels scale_axes = [] # holds scale axes scale_labels = [] # holds scale axis labels # Get tracks to be drawn and set track sizes self.drawn_tracks = self._parent.get_drawn_levels() self.set_track_heights() # Go through each track in the parent (if it is to be drawn) one by # one and collate the data as drawing elements for track_level in self._parent.get_drawn_levels(): self.current_track_level = track_level track = self._parent[track_level] gbgs, glabels = self.draw_greytrack(track) # Greytracks greytrack_bgs.append(gbgs) greytrack_labels.append(glabels) features, flabels = self.draw_track(track) # Features and graphs feature_elements.append(features) feature_labels.append(flabels) if track.scale: axes, slabels = self.draw_scale(track) # Scale axes scale_axes.append(axes) scale_labels.append(slabels) feature_cross_links = [] for cross_link_obj in self.cross_track_links: cross_link_elements = self.draw_cross_link(cross_link_obj) if cross_link_elements: feature_cross_links.append(cross_link_elements) # Groups listed in order of addition to page (from back to front) # Draw track backgrounds # Draw feature cross track links # Draw features and graphs # Draw scale axes # Draw scale labels # Draw feature labels # Draw track labels element_groups = [greytrack_bgs, feature_cross_links, feature_elements, scale_axes, scale_labels, feature_labels, greytrack_labels ] for element_group in element_groups: for element_list in element_group: [self.drawing.add(element) for element in element_list] if self.tracklines: # Draw test tracks over top of diagram self.draw_test_tracks() def draw_track(self, track): """Return list of track elements and list of track labels.""" track_elements = [] # Holds elements for features and graphs track_labels = [] # Holds labels for features and graphs # Distribution dictionary for dealing with different set types set_methods = {FeatureSet: self.draw_feature_set, GraphSet: self.draw_graph_set } for set in track.get_sets(): # Draw the feature or graph sets elements, labels = set_methods[set.__class__](set) track_elements += elements track_labels += labels return track_elements, track_labels def draw_feature_set(self, set): """Return list of feature elements and list of labels for them.""" # print 'draw feature set' feature_elements = [] # Holds diagram elements belonging to the features label_elements = [] # Holds diagram elements belonging to feature labels # Collect all the elements for the feature set for feature in set.get_features(): if self.is_in_bounds(feature.start) or self.is_in_bounds(feature.end): features, labels = self.draw_feature(feature) feature_elements += features label_elements += labels return feature_elements, label_elements def draw_feature(self, feature): """Return list of feature elements and list of labels for them.""" feature_elements = [] # Holds drawable elements for a single feature label_elements = [] # Holds labels for a single feature if feature.hide: # Don't show feature: return early return feature_elements, label_elements start, end = self._current_track_start_end() # A single feature may be split into subfeatures, so loop over them for locstart, locend in feature.locations: if locend < start: continue locstart = max(locstart, start) if end < locstart: continue locend = min(locend, end) # Get sigil for the feature/ each subfeature feature_sigil, label = self.get_feature_sigil(feature, locstart, locend) feature_elements.append(feature_sigil) if label is not None: # If there's a label label_elements.append(label) return feature_elements, label_elements def get_feature_sigil(self, feature, locstart, locend, **kwargs): """Return graphics for feature, and any required label for it. Arguments: - feature Feature object - locstart The start position of the feature - locend The end position of the feature """ # Establish the co-ordinates for the sigil btm, ctr, top = self.track_radii[self.current_track_level] startangle, startcos, startsin = self.canvas_angle(locstart) endangle, endcos, endsin = self.canvas_angle(locend) midangle, midcos, midsin = self.canvas_angle(float(locend + locstart) / 2) # Distribution dictionary for various ways of drawing the feature # Each method takes the inner and outer radii, the start and end angle # subtended at the diagram center, and the color as arguments draw_methods = {'BOX': self._draw_sigil_box, 'OCTO': self._draw_sigil_cut_corner_box, 'JAGGY': self._draw_sigil_jaggy, 'ARROW': self._draw_sigil_arrow, 'BIGARROW': self._draw_sigil_big_arrow, } # Get sigil for the feature, location dependent on the feature strand method = draw_methods[feature.sigil] kwargs['head_length_ratio'] = feature.arrowhead_length kwargs['shaft_height_ratio'] = feature.arrowshaft_height # Support for clickable links... needs ReportLab 2.4 or later # which added support for links in SVG output. if hasattr(feature, "url"): kwargs["hrefURL"] = feature.url kwargs["hrefTitle"] = feature.name sigil = method(btm, ctr, top, startangle, endangle, feature.strand, color=feature.color, border=feature.border, **kwargs) if feature.label: # Feature needs a label # The spaces are a hack to force a little space between the label # and the edge of the feature label = String(0, 0, " %s " % feature.name.strip(), fontName=feature.label_font, fontSize=feature.label_size, fillColor=feature.label_color) labelgroup = Group(label) if feature.label_strand: strand = feature.label_strand else: strand = feature.strand if feature.label_position in ('start', "5'", 'left'): # Position the label at the feature's start if strand != -1: label_angle = startangle + 0.5 * pi # Make text radial sinval, cosval = startsin, startcos else: label_angle = endangle + 0.5 * pi # Make text radial sinval, cosval = endsin, endcos elif feature.label_position in ('middle', 'center', 'centre'): # Position the label at the feature's midpoint label_angle = midangle + 0.5 * pi # Make text radial sinval, cosval = midsin, midcos elif feature.label_position in ('end', "3'", 'right'): # Position the label at the feature's end if strand != -1: label_angle = endangle + 0.5 * pi # Make text radial sinval, cosval = endsin, endcos else: label_angle = startangle + 0.5 * pi # Make text radial sinval, cosval = startsin, startcos elif startangle < pi: # Default to placing the label the bottom of the feature # as drawn on the page, meaning feature end on left half label_angle = endangle + 0.5 * pi # Make text radial sinval, cosval = endsin, endcos else: # Default to placing the label on the bottom of the feature, # which means the feature end when on right hand half label_angle = startangle + 0.5 * pi # Make text radial sinval, cosval = startsin, startcos if strand != -1: # Feature label on top radius = top if startangle < pi: # Turn text round label_angle -= pi else: labelgroup.contents[0].textAnchor = 'end' else: # Feature label on bottom radius = btm if startangle < pi: # Turn text round and anchor end label_angle -= pi labelgroup.contents[0].textAnchor = 'end' x_pos = self.xcenter + radius * sinval y_pos = self.ycenter + radius * cosval coslabel = cos(label_angle) sinlabel = sin(label_angle) labelgroup.transform = (coslabel, -sinlabel, sinlabel, coslabel, x_pos, y_pos) else: # No label required labelgroup = None # if locstart > locend: # print locstart, locend, feature.strand, sigil, feature.name # print locstart, locend, feature.name return sigil, labelgroup def draw_cross_link(self, cross_link): """Draw a cross-link between features.""" startA = cross_link.startA startB = cross_link.startB endA = cross_link.endA endB = cross_link.endB if not self.is_in_bounds(startA) and not self.is_in_bounds(endA): return None if not self.is_in_bounds(startB) and not self.is_in_bounds(endB): return None if startA < self.start: startA = self.start if startB < self.start: startB = self.start if self.end < endA: endA = self.end if self.end < endB: endB = self.end trackobjA = cross_link._trackA(list(self._parent.tracks.values())) trackobjB = cross_link._trackB(list(self._parent.tracks.values())) assert trackobjA is not None assert trackobjB is not None if trackobjA == trackobjB: raise NotImplementedError() if trackobjA.start is not None: if endA < trackobjA.start: return startA = max(startA, trackobjA.start) if trackobjA.end is not None: if trackobjA.end < startA: return endA = min(endA, trackobjA.end) if trackobjB.start is not None: if endB < trackobjB.start: return startB = max(startB, trackobjB.start) if trackobjB.end is not None: if trackobjB.end < startB: return endB = min(endB, trackobjB.end) for track_level in self._parent.get_drawn_levels(): track = self._parent[track_level] if track == trackobjA: trackA = track_level if track == trackobjB: trackB = track_level if trackA == trackB: raise NotImplementedError() startangleA, startcosA, startsinA = self.canvas_angle(startA) startangleB, startcosB, startsinB = self.canvas_angle(startB) endangleA, endcosA, endsinA = self.canvas_angle(endA) endangleB, endcosB, endsinB = self.canvas_angle(endB) btmA, ctrA, topA = self.track_radii[trackA] btmB, ctrB, topB = self.track_radii[trackB] if ctrA < ctrB: return [self._draw_arc_poly(topA, btmB, startangleA, endangleA, startangleB, endangleB, cross_link.color, cross_link.border, cross_link.flip)] else: return [self._draw_arc_poly(btmA, topB, startangleA, endangleA, startangleB, endangleB, cross_link.color, cross_link.border, cross_link.flip)] def draw_graph_set(self, set): """Return list of graph elements and list of their labels. Arguments: - set GraphSet object """ # print 'draw graph set' elements = [] # Holds graph elements # Distribution dictionary for how to draw the graph style_methods = {'line': self.draw_line_graph, 'heat': self.draw_heat_graph, 'bar': self.draw_bar_graph } for graph in set.get_graphs(): elements += style_methods[graph.style](graph) return elements, [] def draw_line_graph(self, graph): """Return line graph as list of drawable elements. Arguments: - graph GraphData object """ line_elements = [] # holds drawable elements # Get graph data data_quartiles = graph.quartiles() minval, maxval = data_quartiles[0], data_quartiles[4] btm, ctr, top = self.track_radii[self.current_track_level] trackheight = 0.5 * (top - btm) datarange = maxval - minval if datarange == 0: datarange = trackheight start, end = self._current_track_start_end() data = graph[start:end] if not data: return [] # midval is the value at which the x-axis is plotted, and is the # central ring in the track if graph.center is None: midval = (maxval + minval) / 2. else: midval = graph.center # Whichever is the greatest difference: max-midval or min-midval, is # taken to specify the number of pixel units resolved along the # y-axis resolution = max((midval - minval), (maxval - midval)) # Start from first data point pos, val = data[0] lastangle, lastcos, lastsin = self.canvas_angle(pos) # We calculate the track height posheight = trackheight * (val - midval) / resolution + ctr lastx = self.xcenter + posheight * lastsin # start xy coords lasty = self.ycenter + posheight * lastcos for pos, val in data: posangle, poscos, possin = self.canvas_angle(pos) posheight = trackheight * (val - midval) / resolution + ctr x = self.xcenter + posheight * possin # next xy coords y = self.ycenter + posheight * poscos line_elements.append(Line(lastx, lasty, x, y, strokeColor=graph.poscolor, strokeWidth=graph.linewidth)) lastx, lasty, = x, y return line_elements def draw_bar_graph(self, graph): """Return list of drawable elements for a bar graph. Arguments: - graph Graph object """ # At each point contained in the graph data, we draw a vertical bar # from the track center to the height of the datapoint value (positive # values go up in one color, negative go down in the alternative # color). bar_elements = [] # Set the number of pixels per unit for the data data_quartiles = graph.quartiles() minval, maxval = data_quartiles[0], data_quartiles[4] btm, ctr, top = self.track_radii[self.current_track_level] trackheight = 0.5 * (top - btm) datarange = maxval - minval if datarange == 0: datarange = trackheight data = graph[self.start:self.end] # midval is the value at which the x-axis is plotted, and is the # central ring in the track if graph.center is None: midval = (maxval + minval) / 2. else: midval = graph.center # Convert data into 'binned' blocks, covering half the distance to the # next data point on either side, accounting for the ends of fragments # and tracks start, end = self._current_track_start_end() data = intermediate_points(start, end, graph[start:end]) if not data: return [] # Whichever is the greatest difference: max-midval or min-midval, is # taken to specify the number of pixel units resolved along the # y-axis resolution = max((midval - minval), (maxval - midval)) if resolution == 0: resolution = trackheight # Create elements for the bar graph based on newdata for pos0, pos1, val in data: pos0angle, pos0cos, pos0sin = self.canvas_angle(pos0) pos1angle, pos1cos, pos1sin = self.canvas_angle(pos1) barval = trackheight * (val - midval) / resolution if barval >= 0: barcolor = graph.poscolor else: barcolor = graph.negcolor # Draw bar bar_elements.append(self._draw_arc(ctr, ctr + barval, pos0angle, pos1angle, barcolor)) return bar_elements def draw_heat_graph(self, graph): """Return list of drawable elements for the heat graph. Arguments: - graph Graph object """ # At each point contained in the graph data, we draw a box that is the # full height of the track, extending from the midpoint between the # previous and current data points to the midpoint between the current # and next data points heat_elements = [] # holds drawable elements # Get graph data data_quartiles = graph.quartiles() minval, maxval = data_quartiles[0], data_quartiles[4] midval = (maxval + minval) / 2. # mid is the value at the X-axis btm, ctr, top = self.track_radii[self.current_track_level] trackheight = (top - btm) start, end = self._current_track_start_end() data = intermediate_points(start, end, graph[start:end]) # Create elements on the graph, indicating a large positive value by # the graph's poscolor, and a large negative value by the graph's # negcolor attributes for pos0, pos1, val in data: pos0angle, pos0cos, pos0sin = self.canvas_angle(pos0) pos1angle, pos1cos, pos1sin = self.canvas_angle(pos1) # Calculate the heat color, based on the differential between # the value and the median value heat = colors.linearlyInterpolatedColor(graph.poscolor, graph.negcolor, maxval, minval, val) # Draw heat box heat_elements.append(self._draw_arc(btm, top, pos0angle, pos1angle, heat, border=heat)) return heat_elements def draw_scale(self, track): """Return list of elements in the scale and list of their labels. Arguments: - track Track object """ scale_elements = [] # holds axes and ticks scale_labels = [] # holds labels if not track.scale: # no scale required, exit early return [], [] # Get track locations btm, ctr, top = self.track_radii[self.current_track_level] trackheight = (top - ctr) # X-axis start, end = self._current_track_start_end() if track.start is not None or track.end is not None: # Draw an arc, leaving out the wedge p = ArcPath(strokeColor=track.scale_color, fillColor=None) startangle, startcos, startsin = self.canvas_angle(start) endangle, endcos, endsin = self.canvas_angle(end) p.addArc(self.xcenter, self.ycenter, ctr, 90 - (endangle * 180 / pi), 90 - (startangle * 180 / pi)) scale_elements.append(p) del p # Y-axis start marker x0, y0 = self.xcenter + btm * startsin, self.ycenter + btm * startcos x1, y1 = self.xcenter + top * startsin, self.ycenter + top * startcos scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) # Y-axis end marker x0, y0 = self.xcenter + btm * endsin, self.ycenter + btm * endcos x1, y1 = self.xcenter + top * endsin, self.ycenter + top * endcos scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) elif self.sweep < 1: # Draw an arc, leaving out the wedge p = ArcPath(strokeColor=track.scale_color, fillColor=None) # Note reportlab counts angles anti-clockwise from the horizontal # (as in mathematics, e.g. complex numbers and polar coordinates) # in degrees. p.addArc(self.xcenter, self.ycenter, ctr, startangledegrees=90 - 360 * self.sweep, endangledegrees=90) scale_elements.append(p) del p # Y-axis start marker x0, y0 = self.xcenter, self.ycenter + btm x1, y1 = self.xcenter, self.ycenter + top scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) # Y-axis end marker alpha = 2 * pi * self.sweep x0, y0 = self.xcenter + btm * sin(alpha), self.ycenter + btm * cos(alpha) x1, y1 = self.xcenter + top * sin(alpha), self.ycenter + top * cos(alpha) scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) else: # Draw a full circle scale_elements.append(Circle(self.xcenter, self.ycenter, ctr, strokeColor=track.scale_color, fillColor=None)) start, end = self._current_track_start_end() if track.scale_ticks: # Ticks are required on the scale # Draw large ticks # I want the ticks to be consistently positioned relative to # the start of the sequence (position 0), not relative to the # current viewpoint (self.start and self.end) ticklen = track.scale_largeticks * trackheight tickiterval = int(track.scale_largetick_interval) # Note that we could just start the list of ticks using # range(0,self.end,tickinterval) and the filter out the # ones before self.start - but this seems wasteful. # Using tickiterval * (self.start/tickiterval) is a shortcut. for tickpos in range(tickiterval * (self.start // tickiterval), int(self.end), tickiterval): if tickpos <= start or end <= tickpos: continue tick, label = self.draw_tick(tickpos, ctr, ticklen, track, track.scale_largetick_labels) scale_elements.append(tick) if label is not None: # If there's a label, add it scale_labels.append(label) # Draw small ticks ticklen = track.scale_smallticks * trackheight tickiterval = int(track.scale_smalltick_interval) for tickpos in range(tickiterval * (self.start // tickiterval), int(self.end), tickiterval): if tickpos <= start or end <= tickpos: continue tick, label = self.draw_tick(tickpos, ctr, ticklen, track, track.scale_smalltick_labels) scale_elements.append(tick) if label is not None: # If there's a label, add it scale_labels.append(label) # Check to see if the track contains a graph - if it does, get the # minimum and maximum values, and put them on the scale Y-axis # at 60 degree intervals, ordering the labels by graph_id startangle, startcos, startsin = self.canvas_angle(start) endangle, endcos, endsin = self.canvas_angle(end) if track.axis_labels: for set in track.get_sets(): if set.__class__ is GraphSet: # Y-axis for n in range(7): angle = n * 1.0471975511965976 if angle < startangle or endangle < angle: continue ticksin, tickcos = sin(angle), cos(angle) x0, y0 = self.xcenter + btm * ticksin, self.ycenter + btm * tickcos x1, y1 = self.xcenter + top * ticksin, self.ycenter + top * tickcos scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color)) graph_label_min = [] graph_label_max = [] graph_label_mid = [] for graph in set.get_graphs(): quartiles = graph.quartiles() minval, maxval = quartiles[0], quartiles[4] if graph.center is None: midval = (maxval + minval) / 2. graph_label_min.append("%.3f" % minval) graph_label_max.append("%.3f" % maxval) graph_label_mid.append("%.3f" % midval) else: diff = max((graph.center - minval), (maxval - graph.center)) minval = graph.center - diff maxval = graph.center + diff midval = graph.center graph_label_mid.append("%.3f" % midval) graph_label_min.append("%.3f" % minval) graph_label_max.append("%.3f" % maxval) xmid, ymid = (x0 + x1) / 2., (y0 + y1) / 2. for limit, x, y, in [(graph_label_min, x0, y0), (graph_label_max, x1, y1), (graph_label_mid, xmid, ymid)]: label = String(0, 0, ";".join(limit), fontName=track.scale_font, fontSize=track.scale_fontsize, fillColor=track.scale_color) label.textAnchor = 'middle' labelgroup = Group(label) labelgroup.transform = (tickcos, -ticksin, ticksin, tickcos, x, y) scale_labels.append(labelgroup) return scale_elements, scale_labels def draw_tick(self, tickpos, ctr, ticklen, track, draw_label): """Return drawing element for a tick on the scale. Arguments: - tickpos Int, position of the tick on the sequence - ctr Float, Y co-ord of the center of the track - ticklen How long to draw the tick - track Track, the track the tick is drawn on - draw_label Boolean, write the tick label? """ # Calculate tick co-ordinates tickangle, tickcos, ticksin = self.canvas_angle(tickpos) x0, y0 = self.xcenter + ctr * ticksin, self.ycenter + ctr * tickcos x1, y1 = self.xcenter + (ctr + ticklen) * ticksin, self.ycenter + (ctr + ticklen) * tickcos # Calculate height of text label so it can be offset on lower half # of diagram # LP: not used, as not all fonts have ascent_descent data in reportlab.pdfbase._fontdata # label_offset = _fontdata.ascent_descent[track.scale_font][0]*\ # track.scale_fontsize/1000. tick = Line(x0, y0, x1, y1, strokeColor=track.scale_color) if draw_label: # Put tick position on as label if track.scale_format == 'SInt': if tickpos >= 1000000: tickstring = str(tickpos // 1000000) + " Mbp" elif tickpos >= 1000: tickstring = str(tickpos // 1000) + " Kbp" else: tickstring = str(tickpos) else: tickstring = str(tickpos) label = String(0, 0, tickstring, # Make label string fontName=track.scale_font, fontSize=track.scale_fontsize, fillColor=track.scale_color) if tickangle > pi: label.textAnchor = 'end' # LP: This label_offset depends on ascent_descent data, which is not available for all # fonts, so has been deprecated. # if 0.5*pi < tickangle < 1.5*pi: # y1 -= label_offset labelgroup = Group(label) labelgroup.transform = (1, 0, 0, 1, x1, y1) else: labelgroup = None return tick, labelgroup def draw_test_tracks(self): """Draw blue test tracks with grene line down their center.""" # Add lines only for drawn tracks for track in self.drawn_tracks: btm, ctr, top = self.track_radii[track] self.drawing.add(Circle(self.xcenter, self.ycenter, top, strokeColor=colors.blue, fillColor=None)) # top line self.drawing.add(Circle(self.xcenter, self.ycenter, ctr, strokeColor=colors.green, fillColor=None)) # middle line self.drawing.add(Circle(self.xcenter, self.ycenter, btm, strokeColor=colors.blue, fillColor=None)) # bottom line def draw_greytrack(self, track): """Drawing element for grey background to passed Track object.""" greytrack_bgs = [] # Holds track backgrounds greytrack_labels = [] # Holds track foreground labels if not track.greytrack: # No greytrack required, return early return [], [] # Get track location btm, ctr, top = self.track_radii[self.current_track_level] start, end = self._current_track_start_end() startangle, startcos, startsin = self.canvas_angle(start) endangle, endcos, endsin = self.canvas_angle(end) # Make background if track.start is not None or track.end is not None: # Draw an arc, leaving out the wedge p = ArcPath(strokeColor=track.scale_color, fillColor=None) greytrack_bgs.append(self._draw_arc(btm, top, startangle, endangle, colors.Color(0.96, 0.96, 0.96))) elif self.sweep < 1: # Make a partial circle, a large arc box # This method assumes the correct center for us. greytrack_bgs.append(self._draw_arc(btm, top, 0, 2 * pi * self.sweep, colors.Color(0.96, 0.96, 0.96))) else: # Make a full circle (using a VERY thick linewidth) greytrack_bgs.append(Circle(self.xcenter, self.ycenter, ctr, strokeColor=colors.Color(0.96, 0.96, 0.96), fillColor=None, strokeWidth=top - btm)) if track.greytrack_labels: # Labels are required for this track labelstep = self.length // track.greytrack_labels # label interval for pos in range(self.start, self.end, labelstep): label = String(0, 0, track.name, # Add a new label at fontName=track.greytrack_font, # each interval fontSize=track.greytrack_fontsize, fillColor=track.greytrack_fontcolor) theta, costheta, sintheta = self.canvas_angle(pos) if theta < startangle or endangle < theta: continue x, y = self.xcenter + btm * sintheta, self.ycenter + btm * costheta # start text halfway up marker labelgroup = Group(label) labelangle = self.sweep * 2 * pi * (pos - self.start) / self.length - pi / 2 if theta > pi: label.textAnchor = 'end' # Anchor end of text to inner radius labelangle += pi # and reorient it cosA, sinA = cos(labelangle), sin(labelangle) labelgroup.transform = (cosA, -sinA, sinA, cosA, x, y) if not self.length - x <= labelstep: # Don't overrun the circle greytrack_labels.append(labelgroup) return greytrack_bgs, greytrack_labels def canvas_angle(self, base): """Given base-pair position, return (angle, cosine, sin) (PRIVATE).""" angle = self.sweep * 2 * pi * (base - self.start) / self.length return (angle, cos(angle), sin(angle)) def _draw_sigil_box(self, bottom, center, top, startangle, endangle, strand, **kwargs): """Draw BOX sigil (PRIVATE).""" if strand == 1: inner_radius = center outer_radius = top elif strand == -1: inner_radius = bottom outer_radius = center else: inner_radius = bottom outer_radius = top return self._draw_arc(inner_radius, outer_radius, startangle, endangle, **kwargs) def _draw_arc(self, inner_radius, outer_radius, startangle, endangle, color, border=None, colour=None, **kwargs): """Return closed path describing an arc box (PRIVATE). Arguments: - inner_radius Float distance of inside of arc from drawing center - outer_radius Float distance of outside of arc from drawing center - startangle Float angle subtended by start of arc at drawing center (in radians) - endangle Float angle subtended by end of arc at drawing center (in radians) - color colors.Color object for arc (overridden by backwards compatible argument with UK spelling, colour). Returns a closed path object describing an arced box corresponding to the passed values. For very small angles, a simple four sided polygon is used. """ # Let the UK spelling (colour) override the USA spelling (color) if colour is not None: color = colour strokecolor, color = _stroke_and_fill_colors(color, border) if abs(float(endangle - startangle)) > .01: # Wide arc, must use full curves p = ArcPath(strokeColor=strokecolor, fillColor=color, strokewidth=0) # Note reportlab counts angles anti-clockwise from the horizontal # (as in mathematics, e.g. complex numbers and polar coordinates) # but we use clockwise from the vertical. Also reportlab uses # degrees, but we use radians. p.addArc(self.xcenter, self.ycenter, inner_radius, 90 - (endangle * 180 / pi), 90 - (startangle * 180 / pi), moveTo=True) p.addArc(self.xcenter, self.ycenter, outer_radius, 90 - (endangle * 180 / pi), 90 - (startangle * 180 / pi), reverse=True) p.closePath() return p else: # Cheat and just use a four sided polygon. # Calculate trig values for angle and coordinates startcos, startsin = cos(startangle), sin(startangle) endcos, endsin = cos(endangle), sin(endangle) x0, y0 = self.xcenter, self.ycenter # origin of the circle x1, y1 = (x0 + inner_radius * startsin, y0 + inner_radius * startcos) x2, y2 = (x0 + inner_radius * endsin, y0 + inner_radius * endcos) x3, y3 = (x0 + outer_radius * endsin, y0 + outer_radius * endcos) x4, y4 = (x0 + outer_radius * startsin, y0 + outer_radius * startcos) return draw_polygon([(x1, y1), (x2, y2), (x3, y3), (x4, y4)], color, border) def _draw_arc_line(self, path, start_radius, end_radius, start_angle, end_angle, move=False): """Add a list of points to a path object (PRIVATE). Assumes angles given are in degrees! Represents what would be a straight line on a linear diagram. """ x0, y0 = self.xcenter, self.ycenter # origin of the circle radius_diff = end_radius - start_radius angle_diff = end_angle - start_angle dx = 0.01 # heuristic a = start_angle * pi / 180 if move: path.moveTo(x0 + start_radius * cos(a), y0 + start_radius * sin(a)) else: path.lineTo(x0 + start_radius * cos(a), y0 + start_radius * sin(a)) x = dx if 0.01 <= abs(dx): while x < 1: r = start_radius + x * radius_diff a = (start_angle + x * (angle_diff)) * pi / 180 # to radians for sin/cos # print x0+r*cos(a), y0+r*sin(a) path.lineTo(x0 + r * cos(a), y0 + r * sin(a)) x += dx a = end_angle * pi / 180 path.lineTo(x0 + end_radius * cos(a), y0 + end_radius * sin(a)) def _draw_arc_poly(self, inner_radius, outer_radius, inner_startangle, inner_endangle, outer_startangle, outer_endangle, color, border=None, flip=False, **kwargs): """Return polygon path describing an arc.""" strokecolor, color = _stroke_and_fill_colors(color, border) x0, y0 = self.xcenter, self.ycenter # origin of the circle if abs(inner_endangle - outer_startangle) > 0.01 \ or abs(outer_endangle - inner_startangle) > 0.01 \ or abs(inner_startangle - outer_startangle) > 0.01 \ or abs(outer_startangle - outer_startangle) > 0.01: # Wide arc, must use full curves p = ArcPath(strokeColor=strokecolor, fillColor=color, # default is mitre/miter which can stick out too much: strokeLineJoin=1, # 1=round strokewidth=0) # Note reportlab counts angles anti-clockwise from the horizontal # (as in mathematics, e.g. complex numbers and polar coordinates) # but we use clockwise from the vertical. Also reportlab uses # degrees, but we use radians. i_start = 90 - (inner_startangle * 180 / pi) i_end = 90 - (inner_endangle * 180 / pi) o_start = 90 - (outer_startangle * 180 / pi) o_end = 90 - (outer_endangle * 180 / pi) p.addArc(x0, y0, inner_radius, i_end, i_start, moveTo=True, reverse=True) if flip: # Flipped, join end to start, self._draw_arc_line(p, inner_radius, outer_radius, i_end, o_start) p.addArc(x0, y0, outer_radius, o_end, o_start, reverse=True) self._draw_arc_line(p, outer_radius, inner_radius, o_end, i_start) else: # Not flipped, join start to start, end to end self._draw_arc_line(p, inner_radius, outer_radius, i_end, o_end) p.addArc(x0, y0, outer_radius, o_end, o_start, reverse=False) self._draw_arc_line(p, outer_radius, inner_radius, o_start, i_start) p.closePath() return p else: # Cheat and just use a four sided polygon. # Calculate trig values for angle and coordinates inner_startcos, inner_startsin = cos(inner_startangle), sin(inner_startangle) inner_endcos, inner_endsin = cos(inner_endangle), sin(inner_endangle) outer_startcos, outer_startsin = cos(outer_startangle), sin(outer_startangle) outer_endcos, outer_endsin = cos(outer_endangle), sin(outer_endangle) x1, y1 = (x0 + inner_radius * inner_startsin, y0 + inner_radius * inner_startcos) x2, y2 = (x0 + inner_radius * inner_endsin, y0 + inner_radius * inner_endcos) x3, y3 = (x0 + outer_radius * outer_endsin, y0 + outer_radius * outer_endcos) x4, y4 = (x0 + outer_radius * outer_startsin, y0 + outer_radius * outer_startcos) return draw_polygon([(x1, y1), (x2, y2), (x3, y3), (x4, y4)], color, border, # default is mitre/miter which can stick out too much: strokeLineJoin=1, # 1=round ) def _draw_sigil_cut_corner_box(self, bottom, center, top, startangle, endangle, strand, color, border=None, corner=0.5, **kwargs): """Draw OCTO sigil, box with corners cut off (PRIVATE).""" if strand == 1: inner_radius = center outer_radius = top elif strand == -1: inner_radius = bottom outer_radius = center else: inner_radius = bottom outer_radius = top strokecolor, color = _stroke_and_fill_colors(color, border) startangle, endangle = min(startangle, endangle), max(startangle, endangle) angle = float(endangle - startangle) middle_radius = 0.5 * (inner_radius + outer_radius) boxheight = outer_radius - inner_radius corner_len = min(0.5 * boxheight, 0.5 * boxheight * corner) shaft_inner_radius = inner_radius + corner_len shaft_outer_radius = outer_radius - corner_len cornerangle_delta = max(0.0, min(abs(boxheight) * 0.5 * corner / middle_radius, abs(angle * 0.5))) if angle < 0: cornerangle_delta *= -1 # reverse it # Calculate trig values for angle and coordinates startcos, startsin = cos(startangle), sin(startangle) endcos, endsin = cos(endangle), sin(endangle) x0, y0 = self.xcenter, self.ycenter # origin of the circle p = ArcPath(strokeColor=strokecolor, fillColor=color, strokeLineJoin=1, # 1=round strokewidth=0, **kwargs) # Inner curved edge p.addArc(self.xcenter, self.ycenter, inner_radius, 90 - ((endangle - cornerangle_delta) * 180 / pi), 90 - ((startangle + cornerangle_delta) * 180 / pi), moveTo=True) # Corner edge - straight lines assumes small angle! # TODO - Use self._draw_arc_line(p, ...) here if we expose corner setting p.lineTo(x0 + shaft_inner_radius * startsin, y0 + shaft_inner_radius * startcos) p.lineTo(x0 + shaft_outer_radius * startsin, y0 + shaft_outer_radius * startcos) # Outer curved edge p.addArc(self.xcenter, self.ycenter, outer_radius, 90 - ((endangle - cornerangle_delta) * 180 / pi), 90 - ((startangle + cornerangle_delta) * 180 / pi), reverse=True) # Corner edges p.lineTo(x0 + shaft_outer_radius * endsin, y0 + shaft_outer_radius * endcos) p.lineTo(x0 + shaft_inner_radius * endsin, y0 + shaft_inner_radius * endcos) p.closePath() return p def _draw_sigil_arrow(self, bottom, center, top, startangle, endangle, strand, **kwargs): """Draw ARROW sigil (PRIVATE).""" if strand == 1: inner_radius = center outer_radius = top orientation = "right" elif strand == -1: inner_radius = bottom outer_radius = center orientation = "left" else: inner_radius = bottom outer_radius = top orientation = "right" # backwards compatibility return self._draw_arc_arrow(inner_radius, outer_radius, startangle, endangle, orientation=orientation, **kwargs) def _draw_sigil_big_arrow(self, bottom, center, top, startangle, endangle, strand, **kwargs): """Draw BIGARROW sigil, like ARROW but straddles the axis (PRIVATE).""" if strand == -1: orientation = "left" else: orientation = "right" return self._draw_arc_arrow(bottom, top, startangle, endangle, orientation=orientation, **kwargs) def _draw_arc_arrow(self, inner_radius, outer_radius, startangle, endangle, color, border=None, shaft_height_ratio=0.4, head_length_ratio=0.5, orientation='right', colour=None, **kwargs): """Draw an arrow along an arc (PRIVATE).""" # Let the UK spelling (colour) override the USA spelling (color) if colour is not None: color = colour strokecolor, color = _stroke_and_fill_colors(color, border) # if orientation == 'right': # startangle, endangle = min(startangle, endangle), max(startangle, endangle) # elif orientation == 'left': # startangle, endangle = max(startangle, endangle), min(startangle, endangle) # else: startangle, endangle = min(startangle, endangle), max(startangle, endangle) if orientation != "left" and orientation != "right": raise ValueError("Invalid orientation %s, should be 'left' or 'right'" % repr(orientation)) angle = float(endangle - startangle) # angle subtended by arc middle_radius = 0.5 * (inner_radius + outer_radius) boxheight = outer_radius - inner_radius shaft_height = boxheight * shaft_height_ratio shaft_inner_radius = middle_radius - 0.5 * shaft_height shaft_outer_radius = middle_radius + 0.5 * shaft_height headangle_delta = max(0.0, min(abs(boxheight) * head_length_ratio / middle_radius, abs(angle))) if angle < 0: headangle_delta *= -1 # reverse it if orientation == "right": headangle = endangle - headangle_delta else: headangle = startangle + headangle_delta if startangle <= endangle: headangle = max(min(headangle, endangle), startangle) else: headangle = max(min(headangle, startangle), endangle) if not (startangle <= headangle <= endangle or endangle <= headangle <= startangle): raise RuntimeError("Problem drawing arrow, invalid positions. " "Start angle: %s, Head angle: %s, " "End angle: %s, Angle: %s" % (startangle, headangle, endangle, angle)) # Calculate trig values for angle and coordinates startcos, startsin = cos(startangle), sin(startangle) headcos, headsin = cos(headangle), sin(headangle) endcos, endsin = cos(endangle), sin(endangle) x0, y0 = self.xcenter, self.ycenter # origin of the circle if 0.5 >= abs(angle) and abs(headangle_delta) >= abs(angle): # If the angle is small, and the arrow is all head, # cheat and just use a triangle. if orientation == "right": x1, y1 = (x0 + inner_radius * startsin, y0 + inner_radius * startcos) x2, y2 = (x0 + outer_radius * startsin, y0 + outer_radius * startcos) x3, y3 = (x0 + middle_radius * endsin, y0 + middle_radius * endcos) else: x1, y1 = (x0 + inner_radius * endsin, y0 + inner_radius * endcos) x2, y2 = (x0 + outer_radius * endsin, y0 + outer_radius * endcos) x3, y3 = (x0 + middle_radius * startsin, y0 + middle_radius * startcos) # return draw_polygon([(x1,y1),(x2,y2),(x3,y3)], color, border, # stroke_line_join=1) return Polygon([x1, y1, x2, y2, x3, y3], strokeColor=border or color, fillColor=color, strokeLineJoin=1, # 1=round, not mitre! strokewidth=0) elif orientation == "right": p = ArcPath(strokeColor=strokecolor, fillColor=color, # default is mitre/miter which can stick out too much: strokeLineJoin=1, # 1=round strokewidth=0, **kwargs) # Note reportlab counts angles anti-clockwise from the horizontal # (as in mathematics, e.g. complex numbers and polar coordinates) # but we use clockwise from the vertical. Also reportlab uses # degrees, but we use radians. p.addArc(self.xcenter, self.ycenter, shaft_inner_radius, 90 - (headangle * 180 / pi), 90 - (startangle * 180 / pi), moveTo=True) p.addArc(self.xcenter, self.ycenter, shaft_outer_radius, 90 - (headangle * 180 / pi), 90 - (startangle * 180 / pi), reverse=True) if abs(angle) < 0.5: p.lineTo(x0 + outer_radius * headsin, y0 + outer_radius * headcos) p.lineTo(x0 + middle_radius * endsin, y0 + middle_radius * endcos) p.lineTo(x0 + inner_radius * headsin, y0 + inner_radius * headcos) else: self._draw_arc_line(p, outer_radius, middle_radius, 90 - (headangle * 180 / pi), 90 - (endangle * 180 / pi)) self._draw_arc_line(p, middle_radius, inner_radius, 90 - (endangle * 180 / pi), 90 - (headangle * 180 / pi)) p.closePath() return p else: p = ArcPath(strokeColor=strokecolor, fillColor=color, # default is mitre/miter which can stick out too much: strokeLineJoin=1, # 1=round strokewidth=0, **kwargs) # Note reportlab counts angles anti-clockwise from the horizontal # (as in mathematics, e.g. complex numbers and polar coordinates) # but we use clockwise from the vertical. Also reportlab uses # degrees, but we use radians. p.addArc(self.xcenter, self.ycenter, shaft_inner_radius, 90 - (endangle * 180 / pi), 90 - (headangle * 180 / pi), moveTo=True, reverse=True) p.addArc(self.xcenter, self.ycenter, shaft_outer_radius, 90 - (endangle * 180 / pi), 90 - (headangle * 180 / pi), reverse=False) # Note - two staight lines is only a good approximation for small # head angle, in general will need to curved lines here: if abs(angle) < 0.5: p.lineTo(x0 + outer_radius * headsin, y0 + outer_radius * headcos) p.lineTo(x0 + middle_radius * startsin, y0 + middle_radius * startcos) p.lineTo(x0 + inner_radius * headsin, y0 + inner_radius * headcos) else: self._draw_arc_line(p, outer_radius, middle_radius, 90 - (headangle * 180 / pi), 90 - (startangle * 180 / pi)) self._draw_arc_line(p, middle_radius, inner_radius, 90 - (startangle * 180 / pi), 90 - (headangle * 180 / pi)) p.closePath() return p def _draw_sigil_jaggy(self, bottom, center, top, startangle, endangle, strand, color, border=None, **kwargs): """Draw JAGGY sigil (PRIVATE). Although we may in future expose the head/tail jaggy lengths, for now both the left and right edges are drawn jagged. """ if strand == 1: inner_radius = center outer_radius = top teeth = 2 elif strand == -1: inner_radius = bottom outer_radius = center teeth = 2 else: inner_radius = bottom outer_radius = top teeth = 4 # TODO, expose these settings? tail_length_ratio = 1.0 head_length_ratio = 1.0 strokecolor, color = _stroke_and_fill_colors(color, border) startangle, endangle = min(startangle, endangle), max(startangle, endangle) angle = float(endangle - startangle) # angle subtended by arc height = outer_radius - inner_radius assert startangle <= endangle and angle >= 0 if head_length_ratio and tail_length_ratio: headangle = max(endangle - min(height * head_length_ratio / (center * teeth), angle * 0.5), startangle) tailangle = min(startangle + min(height * tail_length_ratio / (center * teeth), angle * 0.5), endangle) # With very small features, can due to floating point calculations # violate the assertion below that start <= tail <= head <= end tailangle = min(tailangle, headangle) elif head_length_ratio: headangle = max(endangle - min(height * head_length_ratio / (center * teeth), angle), startangle) tailangle = startangle else: headangle = endangle tailangle = min(startangle + min(height * tail_length_ratio / (center * teeth), angle), endangle) if not startangle <= tailangle <= headangle <= endangle: raise RuntimeError("Problem drawing jaggy sigil, invalid " "positions. Start angle: %s, " "Tail angle: %s, Head angle: %s, End angle %s, " "Angle: %s" % (startangle, tailangle, headangle, endangle, angle)) # Calculate trig values for angle and coordinates startcos, startsin = cos(startangle), sin(startangle) headcos, headsin = cos(headangle), sin(headangle) endcos, endsin = cos(endangle), sin(endangle) x0, y0 = self.xcenter, self.ycenter # origin of the circle p = ArcPath(strokeColor=strokecolor, fillColor=color, # default is mitre/miter which can stick out too much: strokeLineJoin=1, # 1=round strokewidth=0, **kwargs) # Note reportlab counts angles anti-clockwise from the horizontal # (as in mathematics, e.g. complex numbers and polar coordinates) # but we use clockwise from the vertical. Also reportlab uses # degrees, but we use radians. p.addArc(self.xcenter, self.ycenter, inner_radius, 90 - (headangle * 180 / pi), 90 - (tailangle * 180 / pi), moveTo=True) for i in range(0, teeth): p.addArc(self.xcenter, self.ycenter, inner_radius + i * height / teeth, 90 - (tailangle * 180 / pi), 90 - (startangle * 180 / pi)) # Curved line needed when drawing long jaggies self._draw_arc_line(p, inner_radius + i * height / teeth, inner_radius + (i + 1) * height / teeth, 90 - (startangle * 180 / pi), 90 - (tailangle * 180 / pi)) p.addArc(self.xcenter, self.ycenter, outer_radius, 90 - (headangle * 180 / pi), 90 - (tailangle * 180 / pi), reverse=True) for i in range(0, teeth): p.addArc(self.xcenter, self.ycenter, outer_radius - i * height / teeth, 90 - (endangle * 180 / pi), 90 - (headangle * 180 / pi), reverse=True) # Curved line needed when drawing long jaggies self._draw_arc_line(p, outer_radius - i * height / teeth, outer_radius - (i + 1) * height / teeth, 90 - (endangle * 180 / pi), 90 - (headangle * 180 / pi)) p.closePath() return p