mirror of
https://github.com/biopython/biopython.git
synced 2025-10-20 13:43:47 +08:00
$ ruff check --fix --select=I \ --config=lint.isort.force-single-line=true \ --config=lint.isort.order-by-type=false \ BioSQL/ Bio/ Tests/ Scripts/ Doc/ setup.py Using ruff version 0.4.10
568 lines
18 KiB
Python
568 lines
18 KiB
Python
# 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
|
|
################################################################################
|
|
|
|
"""AbstractDrawer module (considered to be a private module, the API may change!).
|
|
|
|
Provides:
|
|
- AbstractDrawer - Superclass for methods common to the Drawer objects
|
|
- page_sizes - Method that returns a ReportLab pagesize when passed
|
|
a valid ISO size
|
|
- draw_box - Method that returns a closed path object when passed
|
|
the proper coordinates. For HORIZONTAL boxes only.
|
|
- angle2trig - Method that returns a tuple of values that are the
|
|
vector for rotating a point through a passed angle,
|
|
about an origin
|
|
- intermediate_points - Method that returns a list of values intermediate
|
|
between the points in a passed dataset
|
|
|
|
For drawing capabilities, this module uses reportlab to draw and write
|
|
the diagram: http://www.reportlab.com
|
|
|
|
For dealing with biological information, the package expects Biopython objects
|
|
like SeqFeatures.
|
|
"""
|
|
|
|
# ReportLab imports
|
|
|
|
from itertools import islice
|
|
from math import cos
|
|
from math import pi
|
|
from math import sin
|
|
|
|
from reportlab.graphics.shapes import Polygon
|
|
from reportlab.lib import colors
|
|
from reportlab.lib import pagesizes
|
|
|
|
################################################################################
|
|
# METHODS
|
|
################################################################################
|
|
|
|
|
|
# Utility method to translate strings to ISO page sizes
|
|
def page_sizes(size):
|
|
"""Convert size string into a Reportlab pagesize.
|
|
|
|
Arguments:
|
|
- size - A string representing a standard page size, eg 'A4' or 'LETTER'
|
|
|
|
"""
|
|
sizes = { # ReportLab pagesizes, keyed by ISO string
|
|
"A0": pagesizes.A0,
|
|
"A1": pagesizes.A1,
|
|
"A2": pagesizes.A2,
|
|
"A3": pagesizes.A3,
|
|
"A4": pagesizes.A4,
|
|
"A5": pagesizes.A5,
|
|
"A6": pagesizes.A6,
|
|
"B0": pagesizes.B0,
|
|
"B1": pagesizes.B1,
|
|
"B2": pagesizes.B2,
|
|
"B3": pagesizes.B3,
|
|
"B4": pagesizes.B4,
|
|
"B5": pagesizes.B5,
|
|
"B6": pagesizes.B6,
|
|
"ELEVENSEVENTEEN": pagesizes.ELEVENSEVENTEEN,
|
|
"LEGAL": pagesizes.LEGAL,
|
|
"LETTER": pagesizes.LETTER,
|
|
}
|
|
try:
|
|
return sizes[size]
|
|
except KeyError:
|
|
raise ValueError(f"{size} not in list of page sizes") from None
|
|
|
|
|
|
def _stroke_and_fill_colors(color, border):
|
|
"""Deal with border and fill colors (PRIVATE)."""
|
|
if not isinstance(color, colors.Color):
|
|
raise ValueError(f"Invalid color {color!r}")
|
|
|
|
if color == colors.white and border is None:
|
|
# Force black border on white boxes with undefined border
|
|
strokecolor = colors.black
|
|
elif border is None:
|
|
strokecolor = color # use fill color
|
|
elif border:
|
|
if not isinstance(border, colors.Color):
|
|
raise ValueError(f"Invalid border color {border!r}")
|
|
strokecolor = border
|
|
else:
|
|
# e.g. False
|
|
strokecolor = None
|
|
|
|
return strokecolor, color
|
|
|
|
|
|
def draw_box(
|
|
point1, point2, color=colors.lightgreen, border=None, colour=None, **kwargs
|
|
):
|
|
"""Draw a box.
|
|
|
|
Arguments:
|
|
- point1, point2 - coordinates for opposite corners of the box
|
|
(x,y tuples)
|
|
- color /colour - The color for the box (colour takes priority
|
|
over color)
|
|
- border - Border color for the box
|
|
|
|
Returns a closed path object, beginning at (x1,y1) going round
|
|
the four points in order, and filling with the passed color.
|
|
"""
|
|
x1, y1 = point1
|
|
x2, y2 = point2
|
|
|
|
# Let the UK spelling (colour) override the USA spelling (color)
|
|
if colour is not None:
|
|
color = colour
|
|
del colour
|
|
|
|
strokecolor, color = _stroke_and_fill_colors(color, border)
|
|
|
|
x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)
|
|
return Polygon(
|
|
[x1, y1, x2, y1, x2, y2, x1, y2],
|
|
strokeColor=strokecolor,
|
|
fillColor=color,
|
|
strokewidth=0,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def draw_cut_corner_box(
|
|
point1, point2, corner=0.5, color=colors.lightgreen, border=None, **kwargs
|
|
):
|
|
"""Draw a box with the corners cut off."""
|
|
x1, y1 = point1
|
|
x2, y2 = point2
|
|
|
|
if not corner:
|
|
return draw_box(point1, point2, color, border)
|
|
elif corner < 0:
|
|
raise ValueError("Arrow head length ratio should be positive")
|
|
|
|
strokecolor, color = _stroke_and_fill_colors(color, border)
|
|
|
|
boxheight = y2 - y1
|
|
boxwidth = x2 - x1
|
|
x_corner = min(boxheight * 0.5 * corner, boxwidth * 0.5)
|
|
y_corner = min(boxheight * 0.5 * corner, boxheight * 0.5)
|
|
|
|
points = [
|
|
x1,
|
|
y1 + y_corner,
|
|
x1,
|
|
y2 - y_corner,
|
|
x1 + x_corner,
|
|
y2,
|
|
x2 - x_corner,
|
|
y2,
|
|
x2,
|
|
y2 - y_corner,
|
|
x2,
|
|
y1 + y_corner,
|
|
x2 - x_corner,
|
|
y1,
|
|
x1 + x_corner,
|
|
y1,
|
|
]
|
|
return Polygon(
|
|
deduplicate(points),
|
|
strokeColor=strokecolor,
|
|
strokeWidth=1,
|
|
strokeLineJoin=1, # 1=round
|
|
fillColor=color,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def draw_polygon(
|
|
list_of_points, color=colors.lightgreen, border=None, colour=None, **kwargs
|
|
):
|
|
"""Draw polygon.
|
|
|
|
Arguments:
|
|
- list_of_point - list of (x,y) tuples for the corner coordinates
|
|
- color / colour - The color for the box
|
|
|
|
Returns a closed path object, beginning at (x1,y1) going round
|
|
the four points in order, and filling with the passed colour.
|
|
|
|
"""
|
|
# Let the UK spelling (colour) override the USA spelling (color)
|
|
if colour is not None:
|
|
color = colour
|
|
del colour
|
|
|
|
strokecolor, color = _stroke_and_fill_colors(color, border)
|
|
|
|
xy_list = []
|
|
for x, y in list_of_points:
|
|
xy_list.append(x)
|
|
xy_list.append(y)
|
|
|
|
return Polygon(
|
|
deduplicate(xy_list),
|
|
strokeColor=strokecolor,
|
|
fillColor=color,
|
|
strokewidth=0,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def draw_arrow(
|
|
point1,
|
|
point2,
|
|
color=colors.lightgreen,
|
|
border=None,
|
|
shaft_height_ratio=0.4,
|
|
head_length_ratio=0.5,
|
|
orientation="right",
|
|
colour=None,
|
|
**kwargs,
|
|
):
|
|
"""Draw an arrow.
|
|
|
|
Returns a closed path object representing an arrow enclosed by the
|
|
box with corners at {point1=(x1,y1), point2=(x2,y2)}, a shaft height
|
|
given by shaft_height_ratio (relative to box height), a head length
|
|
given by head_length_ratio (also relative to box height), and
|
|
an orientation that may be 'left' or 'right'.
|
|
"""
|
|
x1, y1 = point1
|
|
x2, y2 = point2
|
|
|
|
if shaft_height_ratio < 0 or 1 < shaft_height_ratio:
|
|
raise ValueError("Arrow shaft height ratio should be in range 0 to 1")
|
|
if head_length_ratio < 0:
|
|
raise ValueError("Arrow head length ratio should be positive")
|
|
|
|
# Let the UK spelling (colour) override the USA spelling (color)
|
|
if colour is not None:
|
|
color = colour
|
|
del colour
|
|
|
|
strokecolor, color = _stroke_and_fill_colors(color, border)
|
|
|
|
# Depending on the orientation, we define the bottom left (x1, y1) and
|
|
# top right (x2, y2) coordinates differently, but still draw the box
|
|
# using the same relative coordinates:
|
|
xmin, ymin = min(x1, x2), min(y1, y2)
|
|
xmax, ymax = max(x1, x2), max(y1, y2)
|
|
if orientation == "right":
|
|
x1, x2, y1, y2 = xmin, xmax, ymin, ymax
|
|
elif orientation == "left":
|
|
x1, x2, y1, y2 = xmax, xmin, ymin, ymax
|
|
else:
|
|
raise ValueError(
|
|
f"Invalid orientation {orientation!r}, should be 'left' or 'right'"
|
|
)
|
|
|
|
# We define boxheight and boxwidth accordingly, and calculate the shaft
|
|
# height from these. We also ensure that the maximum head length is
|
|
# the width of the box enclosure
|
|
boxheight = y2 - y1
|
|
boxwidth = x2 - x1
|
|
shaftheight = boxheight * shaft_height_ratio
|
|
headlength = min(abs(boxheight) * head_length_ratio, abs(boxwidth))
|
|
if boxwidth < 0:
|
|
headlength *= -1 # reverse it
|
|
|
|
shafttop = 0.5 * (boxheight + shaftheight)
|
|
shaftbase = boxheight - shafttop
|
|
headbase = boxwidth - headlength
|
|
midheight = 0.5 * boxheight
|
|
|
|
points = [
|
|
x1,
|
|
y1 + shafttop,
|
|
x1 + headbase,
|
|
y1 + shafttop,
|
|
x1 + headbase,
|
|
y2,
|
|
x2,
|
|
y1 + midheight,
|
|
x1 + headbase,
|
|
y1,
|
|
x1 + headbase,
|
|
y1 + shaftbase,
|
|
x1,
|
|
y1 + shaftbase,
|
|
]
|
|
|
|
return Polygon(
|
|
deduplicate(points),
|
|
strokeColor=strokecolor,
|
|
# strokeWidth=max(1, int(boxheight/40.)),
|
|
strokeWidth=1,
|
|
# default is mitre/miter which can stick out too much:
|
|
strokeLineJoin=1, # 1=round
|
|
fillColor=color,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def deduplicate(points):
|
|
"""Remove adjacent duplicate points.
|
|
|
|
This is important for use with the Polygon class since reportlab has a
|
|
bug with duplicate points.
|
|
|
|
Arguments:
|
|
- points - list of points [x1, y1, x2, y2,...]
|
|
|
|
Returns a list in the same format with consecutive duplicates removed
|
|
"""
|
|
assert len(points) % 2 == 0
|
|
if len(points) < 2:
|
|
return points
|
|
newpoints = points[0:2]
|
|
for x, y in zip(islice(points, 2, None, 2), islice(points, 3, None, 2)):
|
|
if x != newpoints[-2] or y != newpoints[-1]:
|
|
newpoints.append(x)
|
|
newpoints.append(y)
|
|
return newpoints
|
|
|
|
|
|
def angle2trig(theta):
|
|
"""Convert angle to a reportlab ready tuple.
|
|
|
|
Arguments:
|
|
- theta - Angle in degrees, counter clockwise from horizontal
|
|
|
|
Returns a representation of the passed angle in a format suitable
|
|
for ReportLab rotations (i.e. cos(theta), sin(theta), -sin(theta),
|
|
cos(theta) tuple)
|
|
"""
|
|
c = cos(theta * pi / 180)
|
|
s = sin(theta * pi / 180)
|
|
return (c, s, -s, c) # Vector for rotating point around an origin
|
|
|
|
|
|
def intermediate_points(start, end, graph_data):
|
|
"""Generate intermediate points describing provided graph data..
|
|
|
|
Returns a list of (start, end, value) tuples describing the passed
|
|
graph data as 'bins' between position midpoints.
|
|
"""
|
|
newdata = [] # data in form (X0, X1, val)
|
|
# add first block
|
|
newdata.append(
|
|
(
|
|
start,
|
|
graph_data[0][0] + (graph_data[1][0] - graph_data[0][0]) / 2.0,
|
|
graph_data[0][1],
|
|
)
|
|
)
|
|
# add middle set
|
|
for index in range(1, len(graph_data) - 1):
|
|
lastxval, lastyval = graph_data[index - 1]
|
|
xval, yval = graph_data[index]
|
|
nextxval, nextyval = graph_data[index + 1]
|
|
newdata.append(
|
|
(lastxval + (xval - lastxval) / 2.0, xval + (nextxval - xval) / 2.0, yval)
|
|
)
|
|
# add last block
|
|
newdata.append((xval + (nextxval - xval) / 2.0, end, graph_data[-1][1]))
|
|
return newdata
|
|
|
|
|
|
################################################################################
|
|
# CLASSES
|
|
################################################################################
|
|
|
|
|
|
class AbstractDrawer:
|
|
"""Abstract Drawer.
|
|
|
|
Attributes:
|
|
- tracklines Boolean for whether to draw lines delineating 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
|
|
- 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,
|
|
pagesize="A3",
|
|
orientation="landscape",
|
|
x=0.05,
|
|
y=0.05,
|
|
xl=None,
|
|
xr=None,
|
|
yt=None,
|
|
yb=None,
|
|
start=None,
|
|
end=None,
|
|
tracklines=0,
|
|
cross_track_links=None,
|
|
):
|
|
"""Create the 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)
|
|
- 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
|
|
- cross_track_links List of tuples each with four entries (track A,
|
|
feature A, track B, feature B) to be linked.
|
|
|
|
"""
|
|
self._parent = parent # The calling Diagram object
|
|
|
|
# Perform 'administrative' tasks of setting up the page
|
|
self.set_page_size(pagesize, orientation) # Set drawing size
|
|
self.set_margins(x, y, xl, xr, yt, yb) # Set page margins
|
|
self.set_bounds(start, end) # Set limits on what will be drawn
|
|
self.tracklines = tracklines # Set flags
|
|
if cross_track_links is None:
|
|
cross_track_links = []
|
|
else:
|
|
self.cross_track_links = cross_track_links
|
|
|
|
def set_page_size(self, pagesize, orientation):
|
|
"""Set page size of the drawing..
|
|
|
|
Arguments:
|
|
- pagesize Size of the output image, a tuple of pixels (width,
|
|
height, or a string in the reportlab.lib.pagesizes
|
|
set of ISO sizes.
|
|
- orientation String: 'landscape' or 'portrait'
|
|
|
|
"""
|
|
if isinstance(pagesize, str): # A string, so translate
|
|
pagesize = page_sizes(pagesize)
|
|
elif isinstance(pagesize, tuple): # A tuple, so don't translate
|
|
pass
|
|
else:
|
|
raise ValueError(f"Page size {pagesize} not recognised")
|
|
shortside, longside = min(pagesize), max(pagesize)
|
|
|
|
orientation = orientation.lower()
|
|
if orientation not in ("landscape", "portrait"):
|
|
raise ValueError(f"Orientation {orientation} not recognised")
|
|
if orientation == "landscape":
|
|
self.pagesize = (longside, shortside)
|
|
else:
|
|
self.pagesize = (shortside, longside)
|
|
|
|
def set_margins(self, x, y, xl, xr, yt, yb):
|
|
"""Set page margins.
|
|
|
|
Arguments:
|
|
- x Float(0->1), Absolute X margin as % of page
|
|
- y Float(0->1), Absolute Y margin as % of page
|
|
- xl Float(0->1), Left X margin as % of page
|
|
- xr Float(0->1), Right X margin as % of page
|
|
- yt Float(0->1), Top Y margin as % of page
|
|
- yb Float(0->1), Bottom Y margin as % of page
|
|
|
|
Set the page margins as proportions of the page 0->1, and also
|
|
set the page limits x0, y0 and xlim, ylim, and page center
|
|
xorigin, yorigin, as well as overall page width and height
|
|
"""
|
|
# Set left, right, top and bottom margins
|
|
xmargin_l = xl or x
|
|
xmargin_r = xr or x
|
|
ymargin_top = yt or y
|
|
ymargin_btm = yb or y
|
|
|
|
# Set page limits, center and height/width
|
|
self.x0, self.y0 = self.pagesize[0] * xmargin_l, self.pagesize[1] * ymargin_btm
|
|
self.xlim, self.ylim = (
|
|
self.pagesize[0] * (1 - xmargin_r),
|
|
self.pagesize[1] * (1 - ymargin_top),
|
|
)
|
|
self.pagewidth = self.xlim - self.x0
|
|
self.pageheight = self.ylim - self.y0
|
|
self.xcenter, self.ycenter = (
|
|
self.x0 + self.pagewidth / 2.0,
|
|
self.y0 + self.pageheight / 2.0,
|
|
)
|
|
|
|
def set_bounds(self, start, end):
|
|
"""Set start and end points for the drawing as a whole.
|
|
|
|
Arguments:
|
|
- start - The first base (or feature mark) to draw from
|
|
- end - The last base (or feature mark) to draw to
|
|
|
|
"""
|
|
low, high = self._parent.range() # Extent of tracks
|
|
|
|
if start is not None and end is not None and start > end:
|
|
start, end = end, start
|
|
|
|
if start is None or start < 0: # Check validity of passed args and
|
|
start = 0 # default to 0
|
|
if end is None or end < 0:
|
|
end = high + 1 # default to track range top limit
|
|
|
|
self.start, self.end = int(start), int(end)
|
|
self.length = self.end - self.start + 1
|
|
|
|
def is_in_bounds(self, value):
|
|
"""Check if given value is within the region selected for drawing.
|
|
|
|
Arguments:
|
|
- value - A base position
|
|
|
|
"""
|
|
if value >= self.start and value <= self.end:
|
|
return 1
|
|
return 0
|
|
|
|
def __len__(self):
|
|
"""Return the length of the region to be drawn."""
|
|
return self.length
|
|
|
|
def _current_track_start_end(self):
|
|
track = self._parent[self.current_track_level]
|
|
if track.start is None:
|
|
start = self.start
|
|
else:
|
|
start = max(self.start, track.start)
|
|
if track.end is None:
|
|
end = self.end
|
|
else:
|
|
end = min(self.end, track.end)
|
|
return start, end
|