aakash0017's picture
Upload folder using huggingface_hub
b7731cd
# 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
# [email protected]
################################################################################
"""CircularDrawer module for GenomeDiagram."""
# ReportLab imports
from reportlab.graphics.shapes import Drawing, String, Group, Line, Circle, Polygon
from reportlab.lib import colors
from reportlab.graphics.shapes import ArcPath
# 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
# Determine proportion of circumference around which information will be drawn
if not circular:
self.sweep = 0.9
else:
self.sweep = 1.0
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.0
) # '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.0
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 coordinates 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((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,
f" {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.0
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.0
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.0 # 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.0
graph_label_min.append(f"{minval:.3f}")
graph_label_max.append(f"{maxval:.3f}")
graph_label_mid.append(f"{midval:.3f}")
else:
diff = max(
(graph.center - minval), (maxval - graph.center)
)
minval = graph.center - diff
maxval = graph.center + diff
midval = graph.center
graph_label_mid.append(f"{midval:.3f}")
graph_label_min.append(f"{minval:.3f}")
graph_label_max.append(f"{maxval:.3f}")
xmid, ymid = (x0 + x1) / 2.0, (y0 + y1) / 2.0
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 coordinates
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(endangle - startangle) > 0.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 = 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(
f"Invalid orientation {orientation!r}, should be 'left' or 'right'"
)
angle = 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 = 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