"""Implements the CircularGraphicRecord class.
"""
import matplotlib.patches as mpatches
import numpy as np
from ..GraphicRecord import GraphicRecord
from .ArrowWedge import ArrowWedge
[docs]class CircularGraphicRecord(GraphicRecord):
"""Set of Genetic Features of a same DNA sequence, to be plotted together.
Parameters
----------
sequence_length
Length of the DNA sequence, in number of nucleotides
features
list of GraphicalFeature objects.
top_position
The index in the sequence that will end up at the top of the circle
feature_level_height
Width in inches of one "level" for feature arrows.
annotation_height
Width in inches of one "level" for feature annotations.
labels_spacing
Distance in basepairs to keep between labels to avoid "quasi-collisions"
**kw
Other keyword arguments - do not use, these parameters are allowed for
making it easier to use GraphicRecord and CircularGraphicRecord
interchangeably.
"""
default_elevate_outline_annotations = True
min_y_height_of_text_line = 0.1
def __init__(
self,
sequence_length,
features,
top_position=0,
feature_level_height=0.2,
annotation_height="auto",
labels_spacing=12,
**kw
):
self.radius = 1.0
self.sequence_length = sequence_length
self.features = features
self.top_position = top_position
self.feature_level_height = feature_level_height
self.annotation_height = annotation_height
self.labels_spacing = labels_spacing
[docs] def initialize_ax(self, ax, draw_line, with_ruler):
"""Initialize the ax with a circular line, sets limits, aspect etc.
"""
if draw_line:
circle = mpatches.Circle(
(0, -self.radius), self.radius, facecolor="none", edgecolor="k"
)
ax.add_patch(circle)
ax.axis("off")
if with_ruler:
# only display the xaxis ticks
ax.set_frame_on(False)
ax.yaxis.set_visible(False)
ax.xaxis.tick_bottom()
else:
# don't display anything
ax.axis("off")
ax.set_xlim(-1.1 * self.radius, 1.1 * self.radius)
ax.set_ylim(-self.radius, 3 * self.radius)
ax.set_aspect("equal")
[docs] def finalize_ax(
self,
ax,
features_levels,
annotations_max_level,
auto_figure_height=False,
ideal_yspan=None,
annotations_are_elevated=True
):
"""Final display range and figure dimension tweakings."""
annotation_height = self.determine_annotation_height(
annotations_max_level
)
ymin = -2 * self.radius - self.feature_level_height * (
features_levels + 1
)
ymax = (
self.feature_level_height * (features_levels + 1)
+ annotation_height * (annotations_max_level + 1)
)
if ideal_yspan is not None:
ymax = max(annotation_height * ideal_yspan + ymin, ymax)
xmin = -self.radius - self.feature_level_height * (features_levels + 1)
xmax = -xmin
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)
ratio = 1.0 * (ymax - ymin) / (xmax - xmin)
if auto_figure_height:
figure_width = ax.figure.get_size_inches()[0]
ax.figure.set_size_inches(figure_width, figure_width * ratio)
[docs] def plot_feature(self, ax, feature, level):
"""Plot an ArrowWedge representing the feature at the giben height
level.
"""
a_start = self.position_to_angle(feature.start)
a_end = self.position_to_angle(feature.end)
a_start, a_end = sorted([a_start, a_end])
r = self.radius + level * self.feature_level_height
patch = ArrowWedge(
center=(0, -self.radius),
radius=r,
theta1=a_start,
theta2=a_end,
width=0.7 * self.feature_level_height,
direction=feature.strand,
edgecolor=feature.linecolor,
linewidth=feature.linewidth,
facecolor=feature.color,
zorder=1,
)
ax.add_patch(patch)
[docs] def position_to_angle(self, position):
"""Convert a sequence position into an angle in the figure."""
a = 360.0 * (position - self.top_position) / self.sequence_length
return 90 - a
[docs] def coordinates_in_plot(self, position, level):
"""Convert a sequence position and height level to (x, y) coordinates.
"""
r = self.radius + level * self.feature_level_height
angle = self.position_to_angle(position)
rad_angle = np.deg2rad(angle)
return np.array(
[r * np.cos(rad_angle), r * np.sin(rad_angle) - self.radius]
)
[docs] def determine_annotation_height(self, max_annotations_level):
"""Auto-select the annotations height.
Annotation height is 0.2 at most, or else whatever will make
the figure a 5*radius tall rectangle where the circular plasmid
occupies the bottom-2 5th and the annotations occupy the top-3 5th.
"""
return min(0.25, 3.0 * self.radius / (1.0 + max_annotations_level))
def compute_padding(self, ax):
""
ax_width = ax.get_window_extent(ax.figure.canvas.get_renderer()).width
xmin, xmax = ax.get_xlim()
return 3 * self.labels_spacing * (xmax - xmin) / (1.0 * ax_width)