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, which it should be possible to convert
26 to a wide variety of other formats.
27
28 There is also the start of support for other formats (such as eps), but it
29 doesn't seem to be quite working right for me yet...
30 """
31
32 import os
33
34
35 from reportlab.pdfgen import canvas
36 from reportlab.lib.pagesizes import letter
37 from reportlab.lib.units import inch
38 from reportlab.lib import colors
39
40 from reportlab.graphics.shapes import Drawing, String, Line, Rect, Wedge
41 from reportlab.graphics import renderPDF, renderPS
42 from reportlab.graphics.widgetbase import Widget
43
45 """Base class specifying the interface for a component of the system.
46
47 This class should not be instantiated directly, but should be used
48 from derived classes.
49 """
51 """Initialize a chromosome component.
52
53 Attributes:
54
55 o _sub_components -- Any components which are contained under
56 this parent component. This attribute should be accessed through
57 the add() and remove() functions.
58 """
59 self._sub_components = []
60
61 - def add(self, component):
62 """Add a sub_component to the list of components under this item.
63 """
64 assert isinstance(component, _ChromosomeComponent), \
65 "Expected a _ChromosomeComponent object, got %s" % component
66
67 self._sub_components.append(component)
68
70 """Remove the specified component from the subcomponents.
71
72 Raises a ValueError if the component is not registered as a
73 sub_component.
74 """
75 try:
76 self._sub_components.remove(component)
77 except ValueError:
78 raise ValueError("Component %s not found in sub_components." %
79 component)
80
82 """Draw the specified component.
83 """
84 raise AssertionError("Subclasses must implement.")
85
87 """Top level class for drawing chromosomes.
88
89 This class holds information about an organism and all of it's
90 chromosomes, and provides the top level object which could be used
91 for drawing a chromosome representation of an organism.
92
93 Chromosomes should be added and removed from the Organism via the
94 add and remove functions.
95 """
96 VALID_FORMATS = ['pdf', 'eps']
97
98 - def __init__(self, output_format = 'pdf'):
99 _ChromosomeComponent.__init__(self)
100
101
102 self.page_size = letter
103 self.title_size = 20
104
105 assert output_format in self.VALID_FORMATS, \
106 "Unsupported format: %s, choices are: %s" % (output_format,
107 self.VALID_FORMATS)
108 self.output_format = output_format
109
110 - def draw(self, output_file, title):
111 """Draw out the information for the Organism.
112
113 Arguments:
114
115 o output_file -- The name of a file specifying where the pdf
116 document drawn should be saved.
117
118 o title -- The output title of the produced document.
119 """
120 width, height = self.page_size
121 cur_drawing = Drawing(width, height)
122
123 self._draw_title(cur_drawing, title, width, height)
124
125 cur_x_pos = inch * .5
126 if len(self._sub_components) > 0:
127 x_pos_change = (width - inch) / len(self._sub_components)
128
129 else:
130 pass
131
132 for sub_component in self._sub_components:
133
134 sub_component.start_x_position = cur_x_pos
135 sub_component.end_x_position = cur_x_pos + .9 * x_pos_change
136 sub_component.start_y_position = height - 1.5 * inch
137 sub_component.end_y_position = 3 * inch
138
139
140 sub_component.draw(cur_drawing)
141
142
143 cur_x_pos += x_pos_change
144
145 self._draw_legend(cur_drawing, 2.5 * inch, width)
146
147 if self.output_format == 'pdf':
148 out_canvas = canvas.Canvas(output_file, pagesize = self.page_size)
149 renderPDF.draw(cur_drawing, out_canvas, 0, 0)
150 out_canvas.showPage()
151 out_canvas.save()
152 elif self.output_format == 'eps':
153 renderPS.drawToFile(cur_drawing, output_file)
154 else:
155 raise ValueError("Invalid output format %s" % self.output_format)
156
157 - def _draw_title(self, cur_drawing, title, width, height):
158 """Write out the title of the organism figure.
159 """
160 title_string = String(width / 2, height - inch, title)
161 title_string.fontName = 'Helvetica-Bold'
162 title_string.fontSize = self.title_size
163 title_string.textAnchor = "middle"
164
165 cur_drawing.add(title_string)
166
168 """Draw a legend for the figure.
169
170 Subclasses should implement this to provide specialized legends.
171 """
172 pass
173
175 """Class for drawing a chromosome of an organism.
176
177 This organizes the drawing of a single organisms chromosome. This
178 class can be instantiated directly, but the draw method makes the
179 most sense to be called in the context of an organism.
180 """
182 """Initialize a Chromosome for drawing.
183
184 Arguments:
185
186 o chromosome_name - The label for the chromosome.
187
188 Attributes:
189
190 o start_x_position, end_x_position - The x positions on the page
191 where the chromosome should be drawn. This allows multiple
192 chromosomes to be drawn on a single page.
193
194 o start_y_position, end_y_position - The y positions on the page
195 where the chromosome should be contained.
196
197 Configuration Attributes:
198
199 o title_size - The size of the chromosome title.
200
201 o scale_num - A number of scale the drawing by. This is useful if
202 you want to draw multiple chromosomes of different sizes at the
203 same scale. If this is not set, then the chromosome drawing will
204 be scaled by the number of segements in the chromosome (so each
205 chromosome will be the exact same final size).
206 """
207 _ChromosomeComponent.__init__(self)
208
209 self._name = chromosome_name
210
211 self.start_x_position = -1
212 self.end_x_position = -1
213 self.start_y_position = -1
214 self.end_y_position = -1
215
216 self.title_size = 20
217 self.scale_num = None
218
220 """Return the scaled size of all subcomponents of this component.
221 """
222 total_sub = 0
223 for sub_component in self._sub_components:
224 total_sub += sub_component.scale
225
226 return total_sub
227
228 - def draw(self, cur_drawing):
229 """Draw a chromosome on the specified template.
230
231 Ideally, the x_position and y_*_position attributes should be
232 set prior to drawing -- otherwise we're going to have some problems.
233 """
234 for position in (self.start_x_position, self.end_x_position,
235 self.start_y_position, self.end_y_position):
236 assert position != -1, "Need to set drawing coordinates."
237
238
239
240 cur_y_pos = self.start_y_position
241 if self.scale_num:
242 y_pos_change = ((self.start_y_position * .95 - self.end_y_position)
243 / self.scale_num)
244 elif len(self._sub_components) > 0:
245 y_pos_change = ((self.start_y_position * .95 - self.end_y_position)
246 / self.subcomponent_size())
247
248 else:
249 pass
250
251 for sub_component in self._sub_components:
252 this_y_pos_change = sub_component.scale * y_pos_change
253
254
255 sub_component.start_x_position = self.start_x_position
256 sub_component.end_x_position = self.end_x_position
257 sub_component.start_y_position = cur_y_pos
258 sub_component.end_y_position = cur_y_pos - this_y_pos_change
259
260
261 sub_component.draw(cur_drawing)
262
263
264 cur_y_pos -= this_y_pos_change
265
266 self._draw_label(cur_drawing, self._name)
267
269 """Draw a label for the chromosome.
270 """
271 x_position = self.start_x_position
272 y_position = self.end_y_position
273
274 label_string = String(x_position, y_position, label_name)
275 label_string.fontName = 'Times-BoldItalic'
276 label_string.fontSize = self.title_size
277 label_string.textAnchor = 'start'
278
279 cur_drawing.add(label_string)
280
282 """Draw a segment of a chromosome.
283
284 This class provides the important configurable functionality of drawing
285 a Chromosome. Each segment has some customization available here, or can
286 be subclassed to define additional functionality. Most of the interesting
287 drawing stuff is likely to happen at the ChromosomeSegment level.
288 """
290 """Initialize a ChromosomeSegment.
291
292 Attributes:
293 o start_x_position, end_x_position - Defines the x range we have
294 to draw things in.
295
296 o start_y_position, end_y_position - Defines the y range we have
297 to draw things in.
298
299 Configuration Attributes:
300
301 o scale - A scaling value for the component. By default this is
302 set at 1 (ie -- has the same scale as everything else). Higher
303 values give more size to the component, smaller values give less.
304
305 o fill_color - A color to fill in the segment with. Colors are
306 available in reportlab.lib.colors
307
308 o label - A label to place on the chromosome segment. This should
309 be a text string specifying what is to be included in the label.
310
311 o label_size - The size of the label.
312
313 o chr_percent - The percentage of area that the chromosome
314 segment takes up.
315 """
316 _ChromosomeComponent.__init__(self)
317
318 self.start_x_position = -1
319 self.end_x_position = -1
320 self.start_y_position = -1
321 self.end_y_position = -1
322
323
324 self.scale = 1
325 self.fill_color = None
326 self.label = None
327 self.label_size = 6
328 self.chr_percent = .25
329
330 - def draw(self, cur_drawing):
331 """Draw a chromosome segment.
332
333 Before drawing, the range we are drawing in needs to be set.
334 """
335 for position in (self.start_x_position, self.end_x_position,
336 self.start_y_position, self.end_y_position):
337 assert position != -1, "Need to set drawing coordinates."
338
339 self._draw_subcomponents(cur_drawing)
340 self._draw_segment(cur_drawing)
341 self._draw_label(cur_drawing)
342
344 """Draw any subcomponents of the chromosome segment.
345
346 This should be overridden in derived classes if there are
347 subcomponents to be drawn.
348 """
349 pass
350
352 """Draw the current chromosome segment.
353 """
354
355
356 segment_x = self.start_x_position
357 segment_y = self.end_y_position
358 segment_width = (self.end_x_position - self.start_x_position) \
359 * self.chr_percent
360 segment_height = self.start_y_position - self.end_y_position
361
362
363 right_line = Line(segment_x, segment_y,
364 segment_x, segment_y + segment_height)
365 left_line = Line(segment_x + segment_width, segment_y,
366 segment_x + segment_width, segment_y + segment_height)
367
368 cur_drawing.add(right_line)
369 cur_drawing.add(left_line)
370
371
372 if self.fill_color is not None:
373 fill_rectangle = Rect(segment_x, segment_y,
374 segment_width, segment_height)
375 fill_rectangle.fillColor = self.fill_color
376 fill_rectangle.strokeColor = None
377
378 cur_drawing.add(fill_rectangle)
379
381 """Add a label to the chromosome segment.
382 """
383
384 if self.label is not None:
385
386 label_x = self.start_x_position + \
387 (self.chr_percent + 0.05) * (self.end_x_position -
388 self.start_x_position)
389 label_y = ((self.start_y_position - self.end_y_position) / 2 +
390 self.end_y_position)
391
392 label_string = String(label_x, label_y, self.label)
393 label_string.fontName = 'Helvetica'
394 label_string.fontSize = self.label_size
395
396 cur_drawing.add(label_string)
397
399 """A segment that is located at the end of a linear chromosome.
400
401 This is just like a regular segment, but it draws the end of a chromosome
402 which is represented by a half circle. This just overrides the
403 _draw_segment class of ChromosomeSegment to provide that specialized
404 drawing.
405 """
407 """Initialize a segment at the end of a chromosome.
408
409 See ChromosomeSegment for all of the attributes that can be
410 customized in a TelomereSegments.
411
412 Arguments:
413
414 o inverted -- Whether or not the telomere should be inverted
415 (ie. drawn on the bottom of a chromosome)
416 """
417 ChromosomeSegment.__init__(self)
418
419 self._inverted = inverted
420
422 """Draw a half circle representing the end of a linear chromosome.
423 """
424
425
426 width = (self.end_x_position - self.start_x_position) \
427 * self.chr_percent
428 height = self.start_y_position - self.end_y_position
429
430 center_x = self.start_x_position + width / 2
431 if self._inverted:
432 center_y = self.start_y_position
433 start_angle = 180
434 end_angle = 360
435 else:
436 center_y = self.end_y_position
437 start_angle = 0
438 end_angle = 180
439
440 cap_wedge = Wedge(center_x, center_y, width / 2,
441 start_angle, end_angle, height / 2)
442
443 cap_wedge.fillColor = self.fill_color
444 cur_drawing.add(cap_wedge)
445
446
447 if self._inverted:
448 cover_line = Line(self.start_x_position, self.start_y_position,
449 self.start_x_position + width,
450 self.start_y_position)
451 else:
452 cover_line = Line(self.start_x_position, self.end_y_position,
453 self.start_x_position + width,
454 self.end_y_position)
455
456 if self.fill_color is not None:
457 cover_color = self.fill_color
458 else:
459 cover_color = colors.white
460
461 cover_line.strokeColor = cover_color
462 cur_drawing.add(cover_line)
463