Spaces:
No application file
No application file
# 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. | |
""" | |
# reportlab | |
from reportlab.lib.pagesizes import letter | |
from reportlab.lib.units import inch | |
from reportlab.lib import colors | |
from reportlab.pdfbase.pdfmetrics import stringWidth | |
from reportlab.graphics.shapes import Drawing, String, Line, Rect, Wedge, ArcPath | |
from reportlab.graphics.widgetbase import Widget | |
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. | |
""" | |
pass | |
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. | |
""" | |
pass | |
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. | |
""" | |
pass | |
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.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. | |
""" | |
pass | |