1 """Draw representations of organism chromosomes with added information.
2
3 These classes are meant to model the drawing of pictures of chromosomes.
4 This can be useful for lots of things, including displaying markers on
5 a chromosome (ie. for genetic mapping) and showing syteny between two
6 chromosomes.
7
8 The structure of these classes is intended to be a Composite, so that
9 it will be easy to plug in and switch different parts without
10 breaking the general drawing capabilities of the system. The
11 relationship between classes is that everything derives from
12 _ChromosomeComponent, which specifies the overall interface. The parts
13 then are related so that an Organism contains Chromosomes, and these
14 Chromosomes contain ChromosomeSegments. This representation differents
15 from the canonical composite structure in that we don't really have
16 'leaf' nodes here -- all components can potentially hold sub-components.
17
18 Most of the time the ChromosomeSegment class is what you'll want to
19 customize for specific drawing tasks.
20
21 For providing drawing capabilities, these classes use reportlab:
22
23 http://www.reportlab.com
24
25 This provides nice output in PDF, SVG and postscript. If you have
26 reportlab's renderPM module installed you can also use PNG etc.
27 """
28
29 import os
30
31
32 from reportlab.pdfgen import canvas
33 from reportlab.lib.pagesizes import letter
34 from reportlab.lib.units import inch
35 from reportlab.lib import colors
36
37 from reportlab.graphics.shapes import Drawing, String, Line, Rect, Wedge, ArcPath
38 from reportlab.graphics import renderPDF, renderPS
39 from reportlab.graphics.widgetbase import Widget
40
41 from Bio.Graphics import _write
42 from Bio.Graphics.GenomeDiagram._Colors import ColorTranslator as _ColorTranslator
43
44 _color_trans = _ColorTranslator()
45
46
48 """Base class specifying the interface for a component of the system.
49
50 This class should not be instantiated directly, but should be used
51 from derived classes.
52 """
54 """Initialize a chromosome component.
55
56 Attributes:
57
58 o _sub_components -- Any components which are contained under
59 this parent component. This attribute should be accessed through
60 the add() and remove() functions.
61 """
62 self._sub_components = []
63
64 - def add(self, component):
65 """Add a sub_component to the list of components under this item.
66 """
67 assert isinstance(component, _ChromosomeComponent), \
68 "Expected a _ChromosomeComponent object, got %s" % component
69
70 self._sub_components.append(component)
71
73 """Remove the specified component from the subcomponents.
74
75 Raises a ValueError if the component is not registered as a
76 sub_component.
77 """
78 try:
79 self._sub_components.remove(component)
80 except ValueError:
81 raise ValueError("Component %s not found in sub_components." %
82 component)
83
85 """Draw the specified component.
86 """
87 raise AssertionError("Subclasses must implement.")
88
89
91 """Top level class for drawing chromosomes.
92
93 This class holds information about an organism and all of it's
94 chromosomes, and provides the top level object which could be used
95 for drawing a chromosome representation of an organism.
96
97 Chromosomes should be added and removed from the Organism via the
98 add and remove functions.
99 """
100 - def __init__(self, output_format = 'pdf'):
101 _ChromosomeComponent.__init__(self)
102
103
104 self.page_size = letter
105 self.title_size = 20
106
107
108
109 self._legend_height = 0
110
111 self.output_format = output_format
112
113 - def draw(self, output_file, title):
114 """Draw out the information for the Organism.
115
116 Arguments:
117
118 o output_file -- The name of a file specifying where the
119 document should be saved, or a handle to be written to.
120 The output format is set when creating the Organism object.
121
122 o title -- The output title of the produced document.
123 """
124 width, height = self.page_size
125 cur_drawing = Drawing(width, height)
126
127 self._draw_title(cur_drawing, title, width, height)
128
129 cur_x_pos = inch * .5
130 if len(self._sub_components) > 0:
131 x_pos_change = (width - inch) / len(self._sub_components)
132
133 else:
134 pass
135
136 for sub_component in self._sub_components:
137
138 sub_component.start_x_position = cur_x_pos + 0.05 * x_pos_change
139 sub_component.end_x_position = cur_x_pos + 0.95 * x_pos_change
140 sub_component.start_y_position = height - 1.5 * inch
141 sub_component.end_y_position = self._legend_height + 1 * inch
142
143
144 sub_component.draw(cur_drawing)
145
146
147 cur_x_pos += x_pos_change
148
149 self._draw_legend(cur_drawing, self._legend_height + 0.5 * inch, width)
150
151 return _write(cur_drawing, output_file, self.output_format)
152
153 - def _draw_title(self, cur_drawing, title, width, height):
154 """Write out the title of the organism figure.
155 """
156 title_string = String(width / 2, height - inch, title)
157 title_string.fontName = 'Helvetica-Bold'
158 title_string.fontSize = self.title_size
159 title_string.textAnchor = "middle"
160
161 cur_drawing.add(title_string)
162
164 """Draw a legend for the figure.
165
166 Subclasses should implement this (see also self._legend_height) to
167 provide specialized legends.
168 """
169 pass
170
171
173 """Class for drawing a chromosome of an organism.
174
175 This organizes the drawing of a single organisms chromosome. This
176 class can be instantiated directly, but the draw method makes the
177 most sense to be called in the context of an organism.
178 """
180 """Initialize a Chromosome for drawing.
181
182 Arguments:
183
184 o chromosome_name - The label for the chromosome.
185
186 Attributes:
187
188 o start_x_position, end_x_position - The x positions on the page
189 where the chromosome should be drawn. This allows multiple
190 chromosomes to be drawn on a single page.
191
192 o start_y_position, end_y_position - The y positions on the page
193 where the chromosome should be contained.
194
195 Configuration Attributes:
196
197 o title_size - The size of the chromosome title.
198
199 o scale_num - A number of scale the drawing by. This is useful if
200 you want to draw multiple chromosomes of different sizes at the
201 same scale. If this is not set, then the chromosome drawing will
202 be scaled by the number of segements in the chromosome (so each
203 chromosome will be the exact same final size).
204 """
205 _ChromosomeComponent.__init__(self)
206
207 self._name = chromosome_name
208
209 self.start_x_position = -1
210 self.end_x_position = -1
211 self.start_y_position = -1
212 self.end_y_position = -1
213
214 self.title_size = 20
215 self.scale_num = None
216
217 self.label_size = 6
218 self.chr_percent = 0.25
219 self.label_sep_percent = self.chr_percent * 0.5
220 self._color_labels = False
221
223 """Return the scaled size of all subcomponents of this component.
224 """
225 total_sub = 0
226 for sub_component in self._sub_components:
227 total_sub += sub_component.scale
228
229 return total_sub
230
231 - def draw(self, cur_drawing):
232 """Draw a chromosome on the specified template.
233
234 Ideally, the x_position and y_*_position attributes should be
235 set prior to drawing -- otherwise we're going to have some problems.
236 """
237 for position in (self.start_x_position, self.end_x_position,
238 self.start_y_position, self.end_y_position):
239 assert position != -1, "Need to set drawing coordinates."
240
241
242
243 cur_y_pos = self.start_y_position
244 if self.scale_num:
245 y_pos_change = ((self.start_y_position * .95 - self.end_y_position)
246 / self.scale_num)
247 elif len(self._sub_components) > 0:
248 y_pos_change = ((self.start_y_position * .95 - self.end_y_position)
249 / self.subcomponent_size())
250
251 else:
252 pass
253
254 left_labels = []
255 right_labels = []
256 for sub_component in self._sub_components:
257 this_y_pos_change = sub_component.scale * y_pos_change
258
259
260 sub_component.start_x_position = self.start_x_position
261 sub_component.end_x_position = self.end_x_position
262 sub_component.start_y_position = cur_y_pos
263 sub_component.end_y_position = cur_y_pos - this_y_pos_change
264
265
266 sub_component._left_labels = []
267 sub_component._right_labels = []
268 sub_component.draw(cur_drawing)
269 left_labels += sub_component._left_labels
270 right_labels += sub_component._right_labels
271
272
273 cur_y_pos -= this_y_pos_change
274
275 self._draw_labels(cur_drawing, left_labels, right_labels)
276 self._draw_label(cur_drawing, self._name)
277
279 """Draw a label for the chromosome.
280 """
281 x_position = 0.5 * (self.start_x_position + self.end_x_position)
282 y_position = self.end_y_position
283
284 label_string = String(x_position, y_position, label_name)
285 label_string.fontName = 'Times-BoldItalic'
286 label_string.fontSize = self.title_size
287 label_string.textAnchor = 'middle'
288
289 cur_drawing.add(label_string)
290
291 - def _draw_labels(self, cur_drawing, left_labels, right_labels):
292 """Layout and draw sub-feature labels for the chromosome.
293
294 Tries to place each label at the same vertical position as the
295 feature it applies to, but will adjust the positions to avoid or
296 at least reduce label overlap.
297
298 Draws the label text and a coloured line linking it to the
299 location (i.e. feature) it applies to.
300 """
301 if not self._sub_components:
302 return
303 color_label = self._color_labels
304
305 segment_width = (self.end_x_position - self.start_x_position) \
306 * self.chr_percent
307 label_sep = (self.end_x_position - self.start_x_position) \
308 * self.label_sep_percent
309 segment_x = self.start_x_position \
310 + 0.5 * (self.end_x_position - self.start_x_position - segment_width)
311
312 y_limits = []
313 for sub_component in self._sub_components:
314 y_limits.extend((sub_component.start_y_position, sub_component.end_y_position))
315 y_min = min(y_limits)
316 y_max = max(y_limits)
317 del y_limits
318
319
320
321
322 h = self.label_size
323 left_labels = _place_labels(left_labels, y_min, y_max, h)
324 right_labels = _place_labels(right_labels, y_min, y_max, h)
325 x1 = segment_x
326 x2 = segment_x - label_sep
327 for (y1, y2, color, name) in left_labels:
328 cur_drawing.add(Line(x1, y1, x2, y2,
329 strokeColor = color,
330 strokeWidth = 0.25))
331 label_string = String(x2, y2, name,
332 textAnchor="end")
333 label_string.fontName = 'Helvetica'
334 label_string.fontSize = self.label_size
335 if color_label:
336 label_string.fillColor = color
337 cur_drawing.add(label_string)
338 x1 = segment_x + segment_width
339 x2 = segment_x + segment_width + label_sep
340 for (y1, y2, color, name) in right_labels:
341 cur_drawing.add(Line(x1, y1, x2, y2,
342 strokeColor = color,
343 strokeWidth = 0.25))
344 label_string = String(x2, y2, name)
345 label_string.fontName = 'Helvetica'
346 label_string.fontSize = self.label_size
347 if color_label:
348 label_string.fillColor = color
349 cur_drawing.add(label_string)
350
351
353 """Draw a segment of a chromosome.
354
355 This class provides the important configurable functionality of drawing
356 a Chromosome. Each segment has some customization available here, or can
357 be subclassed to define additional functionality. Most of the interesting
358 drawing stuff is likely to happen at the ChromosomeSegment level.
359 """
361 """Initialize a ChromosomeSegment.
362
363 Attributes:
364 o start_x_position, end_x_position - Defines the x range we have
365 to draw things in.
366
367 o start_y_position, end_y_position - Defines the y range we have
368 to draw things in.
369
370 Configuration Attributes:
371
372 o scale - A scaling value for the component. By default this is
373 set at 1 (ie -- has the same scale as everything else). Higher
374 values give more size to the component, smaller values give less.
375
376 o fill_color - A color to fill in the segment with. Colors are
377 available in reportlab.lib.colors
378
379 o label - A label to place on the chromosome segment. This should
380 be a text string specifying what is to be included in the label.
381
382 o label_size - The size of the label.
383
384 o chr_percent - The percentage of area that the chromosome
385 segment takes up.
386 """
387 _ChromosomeComponent.__init__(self)
388
389 self.start_x_position = -1
390 self.end_x_position = -1
391 self.start_y_position = -1
392 self.end_y_position = -1
393
394
395 self.scale = 1
396 self.fill_color = None
397 self.label = None
398 self.label_size = 6
399 self.chr_percent = .25
400
401 - def draw(self, cur_drawing):
402 """Draw a chromosome segment.
403
404 Before drawing, the range we are drawing in needs to be set.
405 """
406 for position in (self.start_x_position, self.end_x_position,
407 self.start_y_position, self.end_y_position):
408 assert position != -1, "Need to set drawing coordinates."
409
410 self._draw_subcomponents(cur_drawing)
411 self._draw_segment(cur_drawing)
412 self._overdraw_subcomponents(cur_drawing)
413 self._draw_label(cur_drawing)
414
416 """Draw any subcomponents of the chromosome segment.
417
418 This should be overridden in derived classes if there are
419 subcomponents to be drawn.
420 """
421 pass
422
424 """Draw the current chromosome segment.
425 """
426
427
428 segment_y = self.end_y_position
429 segment_width = (self.end_x_position - self.start_x_position) \
430 * self.chr_percent
431 segment_height = self.start_y_position - self.end_y_position
432 segment_x = self.start_x_position \
433 + 0.5 * (self.end_x_position - self.start_x_position - segment_width)
434
435
436 right_line = Line(segment_x, segment_y,
437 segment_x, segment_y + segment_height)
438 left_line = Line(segment_x + segment_width, segment_y,
439 segment_x + segment_width, segment_y + segment_height)
440
441 cur_drawing.add(right_line)
442 cur_drawing.add(left_line)
443
444
445 if self.fill_color is not None:
446 fill_rectangle = Rect(segment_x, segment_y,
447 segment_width, segment_height)
448 fill_rectangle.fillColor = self.fill_color
449 fill_rectangle.strokeColor = None
450
451 cur_drawing.add(fill_rectangle)
452
454 """Draw any subcomponents of the chromosome segment over the main part.
455
456 This should be overridden in derived classes if there are
457 subcomponents to be drawn.
458 """
459 pass
460
462 """Add a label to the chromosome segment.
463
464 The label will be applied to the right of the segment.
465
466 This may be overlapped by any sub-feature labels on other segments!
467 """
468 if self.label is not None:
469
470 label_x = 0.5 * (self.start_x_position + self.end_x_position) + \
471 (self.chr_percent + 0.05) * (self.end_x_position -
472 self.start_x_position)
473 label_y = ((self.start_y_position - self.end_y_position) / 2 +
474 self.end_y_position)
475
476 label_string = String(label_x, label_y, self.label)
477 label_string.fontName = 'Helvetica'
478 label_string.fontSize = self.label_size
479
480 cur_drawing.add(label_string)
481
482
484 """Function to try and layout label co-ordinates (or other floats, PRIVATE).
485
486 Originally written for the y-axis vertical positioning of labels on a
487 chromosome diagram (where the minimum gap between y-axis co-ordinates is
488 the label height), it could also potentially be used for x-axis placement,
489 or indeed radial placement for circular chromosomes within GenomeDiagram.
490
491 In essence this is an optimisation problem, balancing the desire to have
492 each label as close as possible to its data point, but also to spread out
493 the labels to avoid overlaps. This could be described with a cost function
494 (modelling the label distance from the desired placement, and the inter-
495 label separations as springs) and solved as a multi-variable minimization
496 problem - perhaps with NumPy or SciPy.
497
498 For now however, the implementation is a somewhat crude ad hoc algorithm.
499
500 NOTE - This expects the input data to have been sorted!
501 """
502 count = len(desired)
503 if count <= 1:
504 return desired
505 if minimum >= maximum:
506 raise ValueError("Bad min/max %f and %f" % (minimum, maximum))
507 if min(desired) < minimum or max(desired) > maximum:
508 raise ValueError("Data %f to %f out of bounds (%f to %f)"
509 % (min(desired), max(desired), minimum, maximum))
510 equal_step = float(maximum - minimum) / (count - 1)
511
512 if equal_step < gap:
513 import warnings
514 from Bio import BiopythonWarning
515 warnings.warn("Too many labels to avoid overlap", BiopythonWarning)
516
517 return [minimum+i*equal_step for i in range(count)]
518
519 good = True
520 if gap:
521 prev = desired[0]
522 for next in desired[1:]:
523 if prev - next < gap:
524 good = False
525 break
526 if good:
527 return desired
528
529 span = maximum - minimum
530 for split in [0.5*span, span/3.0, 2*span/3.0, 0.25*span, 0.75*span]:
531 midpoint = minimum + split
532 low = [x for x in desired if x <= midpoint - 0.5*gap]
533 high = [x for x in desired if x > midpoint + 0.5*gap]
534 if len(low)+len(high) < count:
535
536 continue
537 elif not low and len(high)*gap <= (span-split) + 0.5*gap:
538
539 return _spring_layout(high, midpoint + 0.5*gap, maximum, gap)
540 elif not high and len(low)*gap <= split + 0.5*gap:
541
542 return _spring_layout(low, minimum, midpoint - 0.5*gap, gap)
543 elif len(low)*gap <= split - 0.5*gap \
544 and len(high)*gap <= (span-split) - 0.5*gap:
545 return _spring_layout(low, minimum, midpoint - 0.5*gap, gap) + \
546 _spring_layout(high, midpoint+ 0.5*gap, maximum, gap)
547
548
549
550
551 low = min(desired)
552 high = max(desired)
553 if (high-low) / (count-1) >= gap:
554
555
556 equal_step = (high-low) / (count-1)
557 return [low+i*equal_step for i in range(count)]
558
559 low = 0.5 * (minimum + min(desired))
560 high = 0.5 * (max(desired) + maximum)
561 if (high-low) / (count-1) >= gap:
562
563 equal_step = (high-low) / (count-1)
564 return [low+i*equal_step for i in range(count)]
565
566
567 return [minimum+i*equal_step for i in range(count)]
568
569
570
571
572
573
575 desired_etc.sort()
576 placed = _spring_layout([row[0] for row in desired_etc],
577 minimum, maximum, gap)
578 for old,y2 in zip(desired_etc, placed):
579 y1, color, name = old
580 yield (y1, y2, color, name)
581
582
584 - def __init__(self, bp_length, features,
585 default_feature_color=colors.blue,
586 name_qualifiers = ['gene', 'label', 'name', 'locus_tag', 'product']):
587 """Like the ChromosomeSegment, but accepts a list of features.
588
589 The features can either be SeqFeature objects, or tuples of five
590 values: start (int), end (int), strand (+1, -1, O or None), label
591 (string) and a ReportLab color.
592
593 Note we require 0 <= start <= end <= bp_length, and within the vertical
594 space allocated to this segmenet lines will be places according to the
595 start/end coordinates (starting from the top).
596
597 Positive stand features are drawn on the right, negative on the left,
598 otherwise all the way across.
599
600 We recommend using consisent units for all the segment's scale values
601 (e.g. their length in base pairs).
602
603 When providing features as SeqFeature objects, the default color
604 is used, unless the feature's qualifiers include an Artemis colour
605 string (functionality also in GenomeDiagram). The caption also follows
606 the GenomeDiagram approach and takes the first qualifier from the list
607 specified in name_qualifiers.
608
609 Note additional attribute label_sep_percent controls the percentage of
610 area that the chromosome segment takes up, by default half of the
611 chr_percent attribute (half of 25%, thus 12.5%)
612
613 """
614 ChromosomeSegment.__init__(self)
615 self.bp_length = bp_length
616 self.features = features
617 self.default_feature_color = default_feature_color
618 self.name_qualifiers = name_qualifiers
619 self.label_sep_percent = self.chr_percent * 0.5
620
622 """Draw any annotated features on the chromosome segment.
623
624 Assumes _draw_segment already called to fill out the basic shape,
625 and assmes that uses the same boundaries.
626 """
627
628
629 segment_y = self.end_y_position
630 segment_width = (self.end_x_position - self.start_x_position) \
631 * self.chr_percent
632 label_sep = (self.end_x_position - self.start_x_position) \
633 * self.label_sep_percent
634 segment_height = self.start_y_position - self.end_y_position
635 segment_x = self.start_x_position \
636 + 0.5 * (self.end_x_position - self.start_x_position - segment_width)
637
638 left_labels = []
639 right_labels = []
640 for f in self.features:
641 try:
642
643 start = f.location.start
644 end = f.location.end
645 strand = f.strand
646 try:
647
648 color = _color_trans.artemis_color(
649 f.qualifiers['color'][0])
650 except:
651 color = self.default_feature_color
652 name = ""
653 for qualifier in self.name_qualifiers:
654 if qualifier in f.qualifiers:
655 name = f.qualifiers[qualifier][0]
656 break
657 except AttributeError:
658
659 start, end, strand, name, color = f
660 assert 0 <= start <= end <= self.bp_length
661 if strand == +1 :
662
663 x = segment_x + segment_width * 0.6
664 w = segment_width * 0.4
665 elif strand == -1:
666
667 x = segment_x
668 w = segment_width * 0.4
669 else:
670
671 x = segment_x
672 w = segment_width
673 local_scale = segment_height / self.bp_length
674 fill_rectangle = Rect(x, segment_y + segment_height - local_scale*start,
675 w, local_scale*(start-end))
676 fill_rectangle.fillColor = color
677 fill_rectangle.strokeColor = color
678 cur_drawing.add(fill_rectangle)
679 if name:
680 value = (segment_y + segment_height - local_scale*start, color, name)
681 if strand == -1:
682 self._left_labels.append(value)
683 else:
684 self._right_labels.append(value)
685
686
688 """A segment that is located at the end of a linear chromosome.
689
690 This is just like a regular segment, but it draws the end of a chromosome
691 which is represented by a half circle. This just overrides the
692 _draw_segment class of ChromosomeSegment to provide that specialized
693 drawing.
694 """
696 """Initialize a segment at the end of a chromosome.
697
698 See ChromosomeSegment for all of the attributes that can be
699 customized in a TelomereSegments.
700
701 Arguments:
702
703 o inverted -- Whether or not the telomere should be inverted
704 (ie. drawn on the bottom of a chromosome)
705 """
706 ChromosomeSegment.__init__(self)
707
708 self._inverted = inverted
709
711 """Draw a half circle representing the end of a linear chromosome.
712 """
713
714
715 width = (self.end_x_position - self.start_x_position) \
716 * self.chr_percent
717 height = self.start_y_position - self.end_y_position
718 center_x = 0.5 * (self.end_x_position + self.start_x_position)
719 start_x = center_x - 0.5 * width
720 if self._inverted:
721 center_y = self.start_y_position
722 start_angle = 180
723 end_angle = 360
724 else:
725 center_y = self.end_y_position
726 start_angle = 0
727 end_angle = 180
728
729 cap_wedge = Wedge(center_x, center_y, width / 2,
730 start_angle, end_angle, height)
731 cap_wedge.strokeColor = None
732 cap_wedge.fillColor = self.fill_color
733 cur_drawing.add(cap_wedge)
734
735
736
737 cap_arc = ArcPath()
738 cap_arc.addArc(center_x, center_y, width / 2,
739 start_angle, end_angle, height)
740 cur_drawing.add(cap_arc)
741
742
744 """A segment that is located at the end of a linear chromosome.
745
746 Doesn't draw anything, just empty space which can be helpful
747 for layout purposes (e.g. making room for feature labels).
748 """
749
750 - def draw(self, cur_diagram):
752