mirror of
https://github.com/biopython/biopython.git
synced 2025-10-20 13:43:47 +08:00
820 lines
31 KiB
Python
820 lines
31 KiB
Python
# Copyright 2001, 2003 by Brad Chapman. All rights reserved.
|
|
# Revisions copyright 2011 by Peter Cock. All rights reserved.
|
|
#
|
|
# 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.
|
|
"""Draw representations of organism chromosomes with added information.
|
|
|
|
These classes are meant to model the drawing of pictures of chromosomes.
|
|
This can be useful for lots of things, including displaying markers on
|
|
a chromosome (ie. for genetic mapping) and showing syteny between two
|
|
chromosomes.
|
|
|
|
The structure of these classes is intended to be a Composite, so that
|
|
it will be easy to plug in and switch different parts without
|
|
breaking the general drawing capabilities of the system. The
|
|
relationship between classes is that everything derives from
|
|
_ChromosomeComponent, which specifies the overall interface. The parts
|
|
then are related so that an Organism contains Chromosomes, and these
|
|
Chromosomes contain ChromosomeSegments. This representation differs
|
|
from the canonical composite structure in that we don't really have
|
|
'leaf' nodes here -- all components can potentially hold sub-components.
|
|
|
|
Most of the time the ChromosomeSegment class is what you'll want to
|
|
customize for specific drawing tasks.
|
|
|
|
For providing drawing capabilities, these classes use reportlab:
|
|
|
|
http://www.reportlab.com
|
|
|
|
This provides nice output in PDF, SVG and postscript. If you have
|
|
reportlab's renderPM module installed you can also use PNG etc.
|
|
"""
|
|
|
|
from reportlab.graphics.shapes import ArcPath
|
|
from reportlab.graphics.shapes import Drawing
|
|
from reportlab.graphics.shapes import Line
|
|
from reportlab.graphics.shapes import Rect
|
|
from reportlab.graphics.shapes import String
|
|
from reportlab.graphics.shapes import Wedge
|
|
from reportlab.graphics.widgetbase import Widget
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.pagesizes import letter
|
|
from reportlab.lib.units import inch
|
|
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
|
|
from Bio.Graphics import _write
|
|
from Bio.Graphics.GenomeDiagram import _Colors
|
|
|
|
_color_trans = _Colors.ColorTranslator()
|
|
|
|
|
|
class _ChromosomeComponent(Widget):
|
|
"""Base class specifying the interface for a component of the system.
|
|
|
|
This class should not be instantiated directly, but should be used
|
|
from derived classes.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize a chromosome component.
|
|
|
|
Attributes:
|
|
- _sub_components -- Any components which are contained under
|
|
this parent component. This attribute should be accessed through
|
|
the add() and remove() functions.
|
|
|
|
"""
|
|
self._sub_components = []
|
|
|
|
def add(self, component):
|
|
"""Add a sub_component to the list of components under this item."""
|
|
if not isinstance(component, _ChromosomeComponent):
|
|
raise TypeError(f"Expected a _ChromosomeComponent object, got {component}")
|
|
|
|
self._sub_components.append(component)
|
|
|
|
def remove(self, component):
|
|
"""Remove the specified component from the subcomponents.
|
|
|
|
Raises a ValueError if the component is not registered as a
|
|
sub_component.
|
|
"""
|
|
try:
|
|
self._sub_components.remove(component)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Component {component} not found in sub_components."
|
|
) from None
|
|
|
|
def draw(self):
|
|
"""Draw the specified component."""
|
|
raise AssertionError("Subclasses must implement.")
|
|
|
|
|
|
class Organism(_ChromosomeComponent):
|
|
"""Top level class for drawing chromosomes.
|
|
|
|
This class holds information about an organism and all of its
|
|
chromosomes, and provides the top level object which could be used
|
|
for drawing a chromosome representation of an organism.
|
|
|
|
Chromosomes should be added and removed from the Organism via the
|
|
add and remove functions.
|
|
"""
|
|
|
|
def __init__(self, output_format="pdf"):
|
|
"""Initialize the class."""
|
|
_ChromosomeComponent.__init__(self)
|
|
|
|
# customizable attributes
|
|
self.page_size = letter
|
|
self.title_size = 20
|
|
|
|
# Do we need this given we don't draw a legend?
|
|
# If so, should be a public API...
|
|
self._legend_height = 0 # 2 * inch
|
|
|
|
self.output_format = output_format
|
|
|
|
def draw(self, output_file, title):
|
|
"""Draw out the information for the Organism.
|
|
|
|
Arguments:
|
|
- output_file -- The name of a file specifying where the
|
|
document should be saved, or a handle to be written to.
|
|
The output format is set when creating the Organism object.
|
|
Alternatively, output_file=None will return the drawing using
|
|
the low-level ReportLab objects (for further processing, such
|
|
as adding additional graphics, before writing).
|
|
- title -- The output title of the produced document.
|
|
|
|
"""
|
|
width, height = self.page_size
|
|
cur_drawing = Drawing(width, height)
|
|
|
|
self._draw_title(cur_drawing, title, width, height)
|
|
|
|
cur_x_pos = inch * 0.5
|
|
if len(self._sub_components) > 0:
|
|
x_pos_change = (width - inch) / len(self._sub_components)
|
|
# no sub_components
|
|
else:
|
|
pass
|
|
|
|
for sub_component in self._sub_components:
|
|
# set the drawing location of the chromosome
|
|
sub_component.start_x_position = cur_x_pos + 0.05 * x_pos_change
|
|
sub_component.end_x_position = cur_x_pos + 0.95 * x_pos_change
|
|
sub_component.start_y_position = height - 1.5 * inch
|
|
sub_component.end_y_position = self._legend_height + 1 * inch
|
|
|
|
# do the drawing
|
|
sub_component.draw(cur_drawing)
|
|
|
|
# update the locations for the next chromosome
|
|
cur_x_pos += x_pos_change
|
|
|
|
self._draw_legend(cur_drawing, self._legend_height + 0.5 * inch, width)
|
|
|
|
if output_file is None:
|
|
# Let the user take care of writing to the file...
|
|
return cur_drawing
|
|
|
|
return _write(cur_drawing, output_file, self.output_format)
|
|
|
|
def _draw_title(self, cur_drawing, title, width, height):
|
|
"""Write out the title of the organism figure (PRIVATE)."""
|
|
title_string = String(width / 2, height - inch, title)
|
|
title_string.fontName = "Helvetica-Bold"
|
|
title_string.fontSize = self.title_size
|
|
title_string.textAnchor = "middle"
|
|
|
|
cur_drawing.add(title_string)
|
|
|
|
def _draw_legend(self, cur_drawing, start_y, width):
|
|
"""Draw a legend for the figure (PRIVATE).
|
|
|
|
Subclasses should implement this (see also self._legend_height) to
|
|
provide specialized legends.
|
|
"""
|
|
|
|
|
|
class Chromosome(_ChromosomeComponent):
|
|
"""Class for drawing a chromosome of an organism.
|
|
|
|
This organizes the drawing of a single organisms chromosome. This
|
|
class can be instantiated directly, but the draw method makes the
|
|
most sense to be called in the context of an organism.
|
|
"""
|
|
|
|
def __init__(self, chromosome_name):
|
|
"""Initialize a Chromosome for drawing.
|
|
|
|
Arguments:
|
|
- chromosome_name - The label for the chromosome.
|
|
|
|
Attributes:
|
|
- start_x_position, end_x_position - The x positions on the page
|
|
where the chromosome should be drawn. This allows multiple
|
|
chromosomes to be drawn on a single page.
|
|
- start_y_position, end_y_position - The y positions on the page
|
|
where the chromosome should be contained.
|
|
|
|
Configuration Attributes:
|
|
- title_size - The size of the chromosome title.
|
|
- scale_num - A number of scale the drawing by. This is useful if
|
|
you want to draw multiple chromosomes of different sizes at the
|
|
same scale. If this is not set, then the chromosome drawing will
|
|
be scaled by the number of segments in the chromosome (so each
|
|
chromosome will be the exact same final size).
|
|
|
|
"""
|
|
_ChromosomeComponent.__init__(self)
|
|
|
|
self._name = chromosome_name
|
|
|
|
self.start_x_position = -1
|
|
self.end_x_position = -1
|
|
self.start_y_position = -1
|
|
self.end_y_position = -1
|
|
|
|
self.title_size = 20
|
|
self.scale_num = None
|
|
|
|
self.label_size = 6
|
|
self.chr_percent = 0.25
|
|
self.label_sep_percent = self.chr_percent * 0.5
|
|
self._color_labels = False
|
|
|
|
def subcomponent_size(self):
|
|
"""Return the scaled size of all subcomponents of this component."""
|
|
total_sub = 0
|
|
for sub_component in self._sub_components:
|
|
total_sub += sub_component.scale
|
|
|
|
return total_sub
|
|
|
|
def draw(self, cur_drawing):
|
|
"""Draw a chromosome on the specified template.
|
|
|
|
Ideally, the x_position and y_*_position attributes should be
|
|
set prior to drawing -- otherwise we're going to have some problems.
|
|
"""
|
|
for position in (
|
|
self.start_x_position,
|
|
self.end_x_position,
|
|
self.start_y_position,
|
|
self.end_y_position,
|
|
):
|
|
assert position != -1, "Need to set drawing coordinates."
|
|
|
|
# first draw all of the sub-sections of the chromosome -- this
|
|
# will actually be the picture of the chromosome
|
|
cur_y_pos = self.start_y_position
|
|
if self.scale_num:
|
|
y_pos_change = (
|
|
self.start_y_position * 0.95 - self.end_y_position
|
|
) / self.scale_num
|
|
elif len(self._sub_components) > 0:
|
|
y_pos_change = (
|
|
self.start_y_position * 0.95 - self.end_y_position
|
|
) / self.subcomponent_size()
|
|
# no sub_components to draw
|
|
else:
|
|
pass
|
|
|
|
left_labels = []
|
|
right_labels = []
|
|
for sub_component in self._sub_components:
|
|
this_y_pos_change = sub_component.scale * y_pos_change
|
|
|
|
# set the location of the component to draw
|
|
sub_component.start_x_position = self.start_x_position
|
|
sub_component.end_x_position = self.end_x_position
|
|
sub_component.start_y_position = cur_y_pos
|
|
sub_component.end_y_position = cur_y_pos - this_y_pos_change
|
|
|
|
# draw the sub component
|
|
sub_component._left_labels = []
|
|
sub_component._right_labels = []
|
|
sub_component.draw(cur_drawing)
|
|
left_labels += sub_component._left_labels
|
|
right_labels += sub_component._right_labels
|
|
|
|
# update the position for the next component
|
|
cur_y_pos -= this_y_pos_change
|
|
|
|
self._draw_labels(cur_drawing, left_labels, right_labels)
|
|
self._draw_label(cur_drawing, self._name)
|
|
|
|
def _draw_label(self, cur_drawing, label_name):
|
|
"""Draw a label for the chromosome (PRIVATE)."""
|
|
x_position = 0.5 * (self.start_x_position + self.end_x_position)
|
|
y_position = self.end_y_position
|
|
|
|
label_string = String(x_position, y_position, label_name)
|
|
label_string.fontName = "Times-BoldItalic"
|
|
label_string.fontSize = self.title_size
|
|
label_string.textAnchor = "middle"
|
|
|
|
cur_drawing.add(label_string)
|
|
|
|
def _draw_labels(self, cur_drawing, left_labels, right_labels):
|
|
"""Layout and draw sub-feature labels for the chromosome (PRIVATE).
|
|
|
|
Tries to place each label at the same vertical position as the
|
|
feature it applies to, but will adjust the positions to avoid or
|
|
at least reduce label overlap.
|
|
|
|
Draws the label text and a coloured line linking it to the
|
|
location (i.e. feature) it applies to.
|
|
"""
|
|
if not self._sub_components:
|
|
return
|
|
color_label = self._color_labels
|
|
|
|
segment_width = (self.end_x_position - self.start_x_position) * self.chr_percent
|
|
label_sep = (
|
|
self.end_x_position - self.start_x_position
|
|
) * self.label_sep_percent
|
|
segment_x = self.start_x_position + 0.5 * (
|
|
self.end_x_position - self.start_x_position - segment_width
|
|
)
|
|
|
|
y_limits = []
|
|
for sub_component in self._sub_components:
|
|
y_limits.extend(
|
|
(sub_component.start_y_position, sub_component.end_y_position)
|
|
)
|
|
y_min = min(y_limits)
|
|
y_max = max(y_limits)
|
|
del y_limits
|
|
# Now do some label placement magic...
|
|
# from reportlab.pdfbase import pdfmetrics
|
|
# font = pdfmetrics.getFont('Helvetica')
|
|
# h = (font.face.ascent + font.face.descent) * 0.90
|
|
h = self.label_size
|
|
for x1, x2, labels, anchor in [
|
|
(
|
|
segment_x,
|
|
segment_x - label_sep,
|
|
_place_labels(left_labels, y_min, y_max, h),
|
|
"end",
|
|
),
|
|
(
|
|
segment_x + segment_width,
|
|
segment_x + segment_width + label_sep,
|
|
_place_labels(right_labels, y_min, y_max, h),
|
|
"start",
|
|
),
|
|
]:
|
|
for y1, y2, color, back_color, name in labels:
|
|
cur_drawing.add(
|
|
Line(x1, y1, x2, y2, strokeColor=color, strokeWidth=0.25)
|
|
)
|
|
label_string = String(x2, y2, name, textAnchor=anchor)
|
|
label_string.fontName = "Helvetica"
|
|
label_string.fontSize = h
|
|
if color_label:
|
|
label_string.fillColor = color
|
|
if back_color:
|
|
w = stringWidth(name, label_string.fontName, label_string.fontSize)
|
|
if x1 > x2:
|
|
w = w * -1.0
|
|
cur_drawing.add(
|
|
Rect(
|
|
x2,
|
|
y2 - 0.1 * h,
|
|
w,
|
|
h,
|
|
strokeColor=back_color,
|
|
fillColor=back_color,
|
|
)
|
|
)
|
|
cur_drawing.add(label_string)
|
|
|
|
|
|
class ChromosomeSegment(_ChromosomeComponent):
|
|
"""Draw a segment of a chromosome.
|
|
|
|
This class provides the important configurable functionality of drawing
|
|
a Chromosome. Each segment has some customization available here, or can
|
|
be subclassed to define additional functionality. Most of the interesting
|
|
drawing stuff is likely to happen at the ChromosomeSegment level.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize a ChromosomeSegment.
|
|
|
|
Attributes:
|
|
- start_x_position, end_x_position - Defines the x range we have
|
|
to draw things in.
|
|
- start_y_position, end_y_position - Defines the y range we have
|
|
to draw things in.
|
|
|
|
Configuration Attributes:
|
|
- scale - A scaling value for the component. By default this is
|
|
set at 1 (ie -- has the same scale as everything else). Higher
|
|
values give more size to the component, smaller values give less.
|
|
- fill_color - A color to fill in the segment with. Colors are
|
|
available in reportlab.lib.colors
|
|
- label - A label to place on the chromosome segment. This should
|
|
be a text string specifying what is to be included in the label.
|
|
- label_size - The size of the label.
|
|
- chr_percent - The percentage of area that the chromosome
|
|
segment takes up.
|
|
|
|
"""
|
|
_ChromosomeComponent.__init__(self)
|
|
|
|
self.start_x_position = -1
|
|
self.end_x_position = -1
|
|
self.start_y_position = -1
|
|
self.end_y_position = -1
|
|
|
|
# --- attributes for configuration
|
|
self.scale = 1
|
|
self.fill_color = None
|
|
self.label = None
|
|
self.label_size = 6
|
|
self.chr_percent = 0.25
|
|
|
|
def draw(self, cur_drawing):
|
|
"""Draw a chromosome segment.
|
|
|
|
Before drawing, the range we are drawing in needs to be set.
|
|
"""
|
|
for position in (
|
|
self.start_x_position,
|
|
self.end_x_position,
|
|
self.start_y_position,
|
|
self.end_y_position,
|
|
):
|
|
assert position != -1, "Need to set drawing coordinates."
|
|
|
|
self._draw_subcomponents(cur_drawing) # Anything behind
|
|
self._draw_segment(cur_drawing)
|
|
self._overdraw_subcomponents(cur_drawing) # Anything on top
|
|
self._draw_label(cur_drawing)
|
|
|
|
def _draw_subcomponents(self, cur_drawing):
|
|
"""Draw any subcomponents of the chromosome segment (PRIVATE).
|
|
|
|
This should be overridden in derived classes if there are
|
|
subcomponents to be drawn.
|
|
"""
|
|
|
|
def _draw_segment(self, cur_drawing):
|
|
"""Draw the current chromosome segment (PRIVATE)."""
|
|
# set the coordinates of the segment -- it'll take up the MIDDLE part
|
|
# of the space we have.
|
|
segment_y = self.end_y_position
|
|
segment_width = (self.end_x_position - self.start_x_position) * self.chr_percent
|
|
segment_height = self.start_y_position - self.end_y_position
|
|
segment_x = self.start_x_position + 0.5 * (
|
|
self.end_x_position - self.start_x_position - segment_width
|
|
)
|
|
|
|
# first draw the sides of the segment
|
|
right_line = Line(segment_x, segment_y, segment_x, segment_y + segment_height)
|
|
left_line = Line(
|
|
segment_x + segment_width,
|
|
segment_y,
|
|
segment_x + segment_width,
|
|
segment_y + segment_height,
|
|
)
|
|
|
|
cur_drawing.add(right_line)
|
|
cur_drawing.add(left_line)
|
|
|
|
# now draw the box, if it is filled in
|
|
if self.fill_color is not None:
|
|
fill_rectangle = Rect(segment_x, segment_y, segment_width, segment_height)
|
|
fill_rectangle.fillColor = self.fill_color
|
|
fill_rectangle.strokeColor = None
|
|
|
|
cur_drawing.add(fill_rectangle)
|
|
|
|
def _overdraw_subcomponents(self, cur_drawing):
|
|
"""Draw any subcomponents of the chromosome segment over the main part (PRIVATE).
|
|
|
|
This should be overridden in derived classes if there are
|
|
subcomponents to be drawn.
|
|
"""
|
|
|
|
def _draw_label(self, cur_drawing):
|
|
"""Add a label to the chromosome segment (PRIVATE).
|
|
|
|
The label will be applied to the right of the segment.
|
|
|
|
This may be overlapped by any sub-feature labels on other segments!
|
|
"""
|
|
if self.label is not None:
|
|
label_x = 0.5 * (self.start_x_position + self.end_x_position) + (
|
|
self.chr_percent + 0.05
|
|
) * (self.end_x_position - self.start_x_position)
|
|
label_y = (
|
|
self.start_y_position - self.end_y_position
|
|
) / 2 + self.end_y_position
|
|
|
|
label_string = String(label_x, label_y, self.label)
|
|
label_string.fontName = "Helvetica"
|
|
label_string.fontSize = self.label_size
|
|
|
|
cur_drawing.add(label_string)
|
|
|
|
|
|
def _spring_layout(desired, minimum, maximum, gap=0):
|
|
"""Try to layout label coordinates or other floats (PRIVATE).
|
|
|
|
Originally written for the y-axis vertical positioning of labels on a
|
|
chromosome diagram (where the minimum gap between y-axis coordinates is
|
|
the label height), it could also potentially be used for x-axis placement,
|
|
or indeed radial placement for circular chromosomes within GenomeDiagram.
|
|
|
|
In essence this is an optimisation problem, balancing the desire to have
|
|
each label as close as possible to its data point, but also to spread out
|
|
the labels to avoid overlaps. This could be described with a cost function
|
|
(modelling the label distance from the desired placement, and the inter-
|
|
label separations as springs) and solved as a multi-variable minimization
|
|
problem - perhaps with NumPy or SciPy.
|
|
|
|
For now however, the implementation is a somewhat crude ad hoc algorithm.
|
|
|
|
NOTE - This expects the input data to have been sorted!
|
|
"""
|
|
count = len(desired)
|
|
if count <= 1:
|
|
return desired # Easy!
|
|
if minimum >= maximum:
|
|
raise ValueError(f"Bad min/max {minimum:f} and {maximum:f}")
|
|
if min(desired) < minimum or max(desired) > maximum:
|
|
raise ValueError(
|
|
"Data %f to %f out of bounds (%f to %f)"
|
|
% (min(desired), max(desired), minimum, maximum)
|
|
)
|
|
equal_step = (maximum - minimum) / (count - 1)
|
|
|
|
if equal_step < gap:
|
|
import warnings
|
|
|
|
from Bio import BiopythonWarning
|
|
|
|
warnings.warn("Too many labels to avoid overlap", BiopythonWarning)
|
|
# Crudest solution
|
|
return [minimum + i * equal_step for i in range(count)]
|
|
|
|
good = True
|
|
if gap:
|
|
prev = desired[0]
|
|
for next in desired[1:]:
|
|
if prev - next < gap:
|
|
good = False
|
|
break
|
|
if good:
|
|
return desired
|
|
|
|
span = maximum - minimum
|
|
for split in [0.5 * span, span / 3.0, 2 * span / 3.0, 0.25 * span, 0.75 * span]:
|
|
midpoint = minimum + split
|
|
low = [x for x in desired if x <= midpoint - 0.5 * gap]
|
|
high = [x for x in desired if x > midpoint + 0.5 * gap]
|
|
if len(low) + len(high) < count:
|
|
# Bad split point, points right on boundary
|
|
continue
|
|
elif not low and len(high) * gap <= (span - split) + 0.5 * gap:
|
|
# Give a little of the unused low space to the high points
|
|
return _spring_layout(high, midpoint + 0.5 * gap, maximum, gap)
|
|
elif not high and len(low) * gap <= split + 0.5 * gap:
|
|
# Give a little of the unused highspace to the low points
|
|
return _spring_layout(low, minimum, midpoint - 0.5 * gap, gap)
|
|
elif (
|
|
len(low) * gap <= split - 0.5 * gap
|
|
and len(high) * gap <= (span - split) - 0.5 * gap
|
|
):
|
|
return _spring_layout(
|
|
low, minimum, midpoint - 0.5 * gap, gap
|
|
) + _spring_layout(high, midpoint + 0.5 * gap, maximum, gap)
|
|
|
|
# This can be count-productive now we can split out into the telomere or
|
|
# spacer-segment's vertical space...
|
|
# Try not to spread out as far as the min/max unless needed
|
|
low = min(desired)
|
|
high = max(desired)
|
|
if (high - low) / (count - 1) >= gap:
|
|
# Good, we don't need the full range, and can position the
|
|
# min and max exactly as well :)
|
|
equal_step = (high - low) / (count - 1)
|
|
return [low + i * equal_step for i in range(count)]
|
|
|
|
low = 0.5 * (minimum + min(desired))
|
|
high = 0.5 * (max(desired) + maximum)
|
|
if (high - low) / (count - 1) >= gap:
|
|
# Good, we don't need the full range
|
|
equal_step = (high - low) / (count - 1)
|
|
return [low + i * equal_step for i in range(count)]
|
|
|
|
# Crudest solution
|
|
return [minimum + i * equal_step for i in range(count)]
|
|
|
|
|
|
# assert False, _spring_layout([0.10,0.12,0.13,0.14,0.5,0.75, 1.0], 0, 1, 0.1)
|
|
# assert _spring_layout([0.10,0.12,0.13,0.14,0.5,0.75, 1.0], 0, 1, 0.1) == \
|
|
# [0.0, 0.125, 0.25, 0.375, 0.5, 0.75, 1.0]
|
|
# assert _spring_layout([0.10,0.12,0.13,0.14,0.5,0.75, 1.0], 0, 1, 0.1) == \
|
|
# [0.0, 0.16666666666666666, 0.33333333333333331, 0.5,
|
|
# 0.66666666666666663, 0.83333333333333326, 1.0]
|
|
|
|
|
|
def _place_labels(desired_etc, minimum, maximum, gap=0):
|
|
# Want a list of lists/tuples for desired_etc
|
|
desired_etc.sort()
|
|
placed = _spring_layout([row[0] for row in desired_etc], minimum, maximum, gap)
|
|
for old, y2 in zip(desired_etc, placed):
|
|
# (y1, a, b, c, ..., z) --> (y1, y2, a, b, c, ..., z)
|
|
yield (old[0], y2) + tuple(old[1:])
|
|
|
|
|
|
class AnnotatedChromosomeSegment(ChromosomeSegment):
|
|
"""Annotated chromosome segment.
|
|
|
|
This is like the ChromosomeSegment, but accepts a list of features.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
bp_length,
|
|
features,
|
|
default_feature_color=colors.blue,
|
|
name_qualifiers=("gene", "label", "name", "locus_tag", "product"),
|
|
):
|
|
"""Initialize.
|
|
|
|
The features can either be SeqFeature objects, or tuples of values:
|
|
start (int), end (int), strand (+1, -1, O or None), label (string),
|
|
ReportLab color (string or object), and optional ReportLab fill color.
|
|
|
|
Note we require 0 <= start <= end <= bp_length, and within the vertical
|
|
space allocated to this segment lines will be places according to the
|
|
start/end coordinates (starting from the top).
|
|
|
|
Positive stand features are drawn on the right, negative on the left,
|
|
otherwise all the way across.
|
|
|
|
We recommend using consistent units for all the segment's scale values
|
|
(e.g. their length in base pairs).
|
|
|
|
When providing features as SeqFeature objects, the default color
|
|
is used, unless the feature's qualifiers include an Artemis colour
|
|
string (functionality also in GenomeDiagram). The caption also follows
|
|
the GenomeDiagram approach and takes the first qualifier from the list
|
|
or tuple specified in name_qualifiers.
|
|
|
|
Note additional attribute label_sep_percent controls the percentage of
|
|
area that the chromosome segment takes up, by default half of the
|
|
chr_percent attribute (half of 25%, thus 12.5%)
|
|
|
|
"""
|
|
ChromosomeSegment.__init__(self)
|
|
self.bp_length = bp_length
|
|
self.features = features
|
|
self.default_feature_color = default_feature_color
|
|
self.name_qualifiers = name_qualifiers
|
|
self.label_sep_percent = self.chr_percent * 0.5
|
|
|
|
def _overdraw_subcomponents(self, cur_drawing):
|
|
"""Draw any annotated features on the chromosome segment (PRIVATE).
|
|
|
|
Assumes _draw_segment already called to fill out the basic shape,
|
|
and assmes that uses the same boundaries.
|
|
"""
|
|
# set the coordinates of the segment -- it'll take up the MIDDLE part
|
|
# of the space we have.
|
|
segment_y = self.end_y_position
|
|
segment_width = (self.end_x_position - self.start_x_position) * self.chr_percent
|
|
label_sep = (
|
|
self.end_x_position - self.start_x_position
|
|
) * self.label_sep_percent
|
|
segment_height = self.start_y_position - self.end_y_position
|
|
segment_x = self.start_x_position + 0.5 * (
|
|
self.end_x_position - self.start_x_position - segment_width
|
|
)
|
|
|
|
left_labels = []
|
|
right_labels = []
|
|
for f in self.features:
|
|
try:
|
|
# Assume SeqFeature objects
|
|
start = f.location.start
|
|
end = f.location.end
|
|
strand = f.location.strand
|
|
try:
|
|
# Handles Artemis colour integers, HTML colors, etc
|
|
color = _color_trans.translate(f.qualifiers["color"][0])
|
|
except Exception: # TODO: ValueError?
|
|
color = self.default_feature_color
|
|
fill_color = color
|
|
name = ""
|
|
for qualifier in self.name_qualifiers:
|
|
if qualifier in f.qualifiers:
|
|
name = f.qualifiers[qualifier][0]
|
|
break
|
|
except AttributeError:
|
|
# Assume tuple of ints, string, and color
|
|
start, end, strand, name, color = f[:5]
|
|
color = _color_trans.translate(color)
|
|
if len(f) > 5:
|
|
fill_color = _color_trans.translate(f[5])
|
|
else:
|
|
fill_color = color
|
|
assert 0 <= start <= end <= self.bp_length
|
|
if strand == +1:
|
|
# Right side only
|
|
x = segment_x + segment_width * 0.6
|
|
w = segment_width * 0.4
|
|
elif strand == -1:
|
|
# Left side only
|
|
x = segment_x
|
|
w = segment_width * 0.4
|
|
else:
|
|
# Both or neither - full width
|
|
x = segment_x
|
|
w = segment_width
|
|
local_scale = segment_height / self.bp_length
|
|
fill_rectangle = Rect(
|
|
x,
|
|
segment_y + segment_height - local_scale * start,
|
|
w,
|
|
local_scale * (start - end),
|
|
)
|
|
fill_rectangle.fillColor = fill_color
|
|
fill_rectangle.strokeColor = color
|
|
cur_drawing.add(fill_rectangle)
|
|
if name:
|
|
if fill_color == color:
|
|
back_color = None
|
|
else:
|
|
back_color = fill_color
|
|
value = (
|
|
segment_y + segment_height - local_scale * start,
|
|
color,
|
|
back_color,
|
|
name,
|
|
)
|
|
if strand == -1:
|
|
self._left_labels.append(value)
|
|
else:
|
|
self._right_labels.append(value)
|
|
|
|
|
|
class TelomereSegment(ChromosomeSegment):
|
|
"""A segment that is located at the end of a linear chromosome.
|
|
|
|
This is just like a regular segment, but it draws the end of a chromosome
|
|
which is represented by a half circle. This just overrides the
|
|
_draw_segment class of ChromosomeSegment to provide that specialized
|
|
drawing.
|
|
"""
|
|
|
|
def __init__(self, inverted=0):
|
|
"""Initialize a segment at the end of a chromosome.
|
|
|
|
See ChromosomeSegment for all of the attributes that can be
|
|
customized in a TelomereSegments.
|
|
|
|
Arguments:
|
|
- inverted -- Whether or not the telomere should be inverted
|
|
(ie. drawn on the bottom of a chromosome)
|
|
|
|
"""
|
|
ChromosomeSegment.__init__(self)
|
|
|
|
self._inverted = inverted
|
|
|
|
def _draw_segment(self, cur_drawing):
|
|
"""Draw a half circle representing the end of a linear chromosome (PRIVATE)."""
|
|
# set the coordinates of the segment -- it'll take up the MIDDLE part
|
|
# of the space we have.
|
|
width = (self.end_x_position - self.start_x_position) * self.chr_percent
|
|
height = self.start_y_position - self.end_y_position
|
|
center_x = 0.5 * (self.end_x_position + self.start_x_position)
|
|
start_x = center_x - 0.5 * width
|
|
if self._inverted:
|
|
center_y = self.start_y_position
|
|
start_angle = 180
|
|
end_angle = 360
|
|
else:
|
|
center_y = self.end_y_position
|
|
start_angle = 0
|
|
end_angle = 180
|
|
|
|
cap_wedge = Wedge(center_x, center_y, width / 2, start_angle, end_angle, height)
|
|
cap_wedge.strokeColor = None
|
|
cap_wedge.fillColor = self.fill_color
|
|
cur_drawing.add(cap_wedge)
|
|
|
|
# Now draw an arc for the curved edge of the wedge,
|
|
# omitting the flat end.
|
|
cap_arc = ArcPath()
|
|
cap_arc.addArc(center_x, center_y, width / 2, start_angle, end_angle, height)
|
|
cur_drawing.add(cap_arc)
|
|
|
|
|
|
class SpacerSegment(ChromosomeSegment):
|
|
"""A segment that is located at the end of a linear chromosome.
|
|
|
|
Doesn't draw anything, just empty space which can be helpful
|
|
for layout purposes (e.g. making room for feature labels).
|
|
"""
|
|
|
|
def draw(self, cur_diagram):
|
|
"""Draw nothing to the current diagram (dummy method).
|
|
|
|
The segment spacer has no actual image in the diagram,
|
|
so this method therefore does nothing, but is defined
|
|
to match the expected API of the other segment objects.
|
|
"""
|