Skip to content

Instantly share code, notes, and snippets.

@peterjc
Last active January 2, 2016 17:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peterjc/8334631 to your computer and use it in GitHub Desktop.
Save peterjc/8334631 to your computer and use it in GitHub Desktop.
Standalone test case originally Biopython for resting ReportLab under Python 2 and 3. Currently seeing a segmentation fault under Python 2.7, and a TypeError under Python 3.3 when testing on Mac OS X - see http://two.pairlist.net/pipermail/reportlab-users/2014-January/010972.html
from reportlab.pdfgen import canvas
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 import renderPDF, renderPS
from reportlab.graphics.widgetbase import Widget
# The following code is to allow all the Bio.Graphics
# code to deal with the different ReportLab renderers
# and the API quirks consistently.
def _write(drawing, output_file, format, dpi=72):
"""Helper function to standardize output to files (PRIVATE).
Writes the provided drawing out to a file in a prescribed format.
drawing - suitable ReportLab drawing object.
output_file - a handle to write to, or a filename to write to.
format - String indicating output format, one of PS, PDF, SVG,
or provided the ReportLab renderPM module is installed,
one of the bitmap formats JPG, BMP, GIF, PNG, TIFF or TIFF.
The format can be given in any case.
dpi - Resolution (dots per inch) for bitmap formats.
No return value.
"""
from reportlab.graphics import renderPS, renderPDF, renderSVG
try:
from reportlab.graphics import renderPM
except ImportError:
#This is an optional part of ReportLab, so may not be installed.
#We'll raise a missing dependency error if rendering to a
#bitmap format is attempted.
renderPM=None
formatdict = {'PS': renderPS, 'EPS': renderPS,
# not sure which you actually get, PS or EPS, but
# GenomeDiagram used PS while other modules used EPS.
'PDF': renderPDF,
'SVG': renderSVG,
'JPG': renderPM,
'BMP': renderPM,
'GIF': renderPM,
'PNG': renderPM,
'TIFF': renderPM,
'TIF': renderPM
}
try:
#If output is not a string, then .upper() will trigger
#an attribute error...
drawmethod = formatdict[format.upper()] # select drawing method
except (KeyError, AttributeError):
raise ValueError("Output format should be one of %s"
% ", ".join(formatdict))
if drawmethod is None:
#i.e. We wanted renderPM but it isn't installed
#See the import at the top of the function.
from Bio import MissingPythonDependencyError
raise MissingPythonDependencyError(
"Please install ReportLab's renderPM module")
if drawmethod == renderPM:
#This has a different API to the other render objects
return drawmethod.drawToFile(drawing, output_file,
format, dpi=dpi)
else:
return drawmethod.drawToFile(drawing, output_file)
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:
o _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.
"""
assert isinstance(component, _ChromosomeComponent), \
"Expected a _ChromosomeComponent object, got %s" % 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("Component %s not found in sub_components." %
component)
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 it's
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'):
_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:
o 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).
o 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 * .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.
"""
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.
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:
o chromosome_name - The label for the chromosome.
Attributes:
o 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.
o start_y_position, end_y_position - The y positions on the page
where the chromosome should be contained.
Configuration Attributes:
o title_size - The size of the chromosome title.
o 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 segements 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 * .95 - self.end_y_position)
/ self.scale_num)
elif len(self._sub_components) > 0:
y_pos_change = ((self.start_y_position * .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.
"""
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."""
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:
o start_x_position, end_x_position - Defines the x range we have
to draw things in.
o start_y_position, end_y_position - Defines the y range we have
to draw things in.
Configuration Attributes:
o 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.
o fill_color - A color to fill in the segment with. Colors are
available in reportlab.lib.colors
o label - A label to place on the chromosome segment. This should
be a text string specifying what is to be included in the label.
o label_size - The size of the label.
o 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 = .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.
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.
"""
# 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.
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.
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 _place_labels(desired_etc, minimum, maximum, gap=0):
#Simified implemetation:
return sorted(desired_etc)
# hold the chromosome info for testing
# the info is structured as (label, color, scale)
chr1_info = (("", None, 1),
("AC30001", None, 1),
("", colors.blue, 2),
("", colors.blue, 1.5),
("", colors.red, 2),
("AC12345", colors.blue, 1),
("", colors.blue, 1),
("", colors.blue, 1),
("", None, 1))
def load_chromosome(chr_name, chr_segment_info):
"""Load a chromosome and all of its segments."""
cur_chromosome = Chromosome(chr_name)
for label, fill_color, scale in chr_segment_info:
cur_segment = ChromosomeSegment()
if label != "":
cur_segment.label = label
if fill_color is not None:
cur_segment.fill_color = fill_color
cur_segment.scale = scale
cur_chromosome.add(cur_segment)
return cur_chromosome
def simple_organism(filename, format):
"""Output a simple organism to given format."""
test_organism = Organism(format)
test_organism.add(load_chromosome("I", chr1_info))
test_organism.draw(filename, "Test organism")
simple_organism("organism.eps", "ps")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment