1 """Display information distributed across a Chromosome-like object.
2
3 These classes are meant to show the distribution of some kind of information
4 as it changes across any kind of segment. It was designed with chromosome
5 distributions in mind, but could also work for chromosome regions, BAC clones
6 or anything similar.
7
8 Reportlab is used for producing the graphical output.
9 """
10
11 import math
12
13
14 from reportlab.pdfgen import canvas
15 from reportlab.lib.pagesizes import letter
16 from reportlab.lib.units import inch
17 from reportlab.lib import colors
18
19 from reportlab.graphics.shapes import Drawing, String
20 from reportlab.graphics.charts.barcharts import VerticalBarChart
21 from reportlab.graphics.charts.barcharts import BarChartProperties
22 from reportlab.graphics.widgetbase import TypedPropertyCollection
23 from reportlab.graphics import renderPDF, renderPS
24
26 """Display a grouping of distributions on a page.
27
28 This organizes Distributions, and will display them nicely
29 on a single page.
30 """
31 - def __init__(self, output_format = 'pdf'):
32 self.distributions = []
33
34
35 self.number_of_columns = 1
36 self.page_size = letter
37 self.title_size = 20
38
39 self.output_format = output_format
40
41 - def draw(self, output_file, title):
42 """Draw out the distribution information.
43
44 Arguments:
45
46 o output_file - The name of the file to output the information to.
47
48 o title - A title to display on the graphic.
49 """
50 width, height = self.page_size
51 cur_drawing = Drawing(width, height)
52
53 self._draw_title(cur_drawing, title, width, height)
54
55
56 cur_x_pos = inch * .5
57 end_x_pos = width - inch * .5
58 cur_y_pos = height - 1.5 * inch
59 end_y_pos = .5 * inch
60 x_pos_change = ((end_x_pos - cur_x_pos) /
61 float(self.number_of_columns))
62 num_y_rows = math.ceil(float(len(self.distributions))
63 / float(self.number_of_columns))
64 y_pos_change = (cur_y_pos - end_y_pos) / num_y_rows
65
66 self._draw_distributions(cur_drawing, cur_x_pos, x_pos_change,
67 cur_y_pos, y_pos_change, num_y_rows)
68 self._draw_legend(cur_drawing, 2.5 * inch, width)
69
70 if self.output_format == 'pdf':
71 out_canvas = canvas.Canvas(output_file, pagesize = self.page_size)
72 renderPDF.draw(cur_drawing, out_canvas, 0, 0)
73 out_canvas.showPage()
74 out_canvas.save()
75 elif self.output_format == 'eps':
76 renderPS.drawToFile(cur_drawing, output_file)
77 else:
78 raise ValueError("Invalid output format %s" % self.output_format)
79
80
81 - def _draw_title(self, cur_drawing, title, width, height):
82 """Add the title of the figure to the drawing.
83 """
84 title_string = String(width / 2, height - inch, title)
85 title_string.fontName = 'Helvetica-Bold'
86 title_string.fontSize = self.title_size
87 title_string.textAnchor = "middle"
88
89 cur_drawing.add(title_string)
90
91 - def _draw_distributions(self, cur_drawing, start_x_pos, x_pos_change,
92 start_y_pos, y_pos_change, num_y_drawings):
93 """Draw all of the distributions on the page.
94
95 Arguments:
96
97 o cur_drawing - The drawing we are working with.
98
99 o start_x_pos - The x position on the page to start drawing at.
100
101 o x_pos_change - The change in x position between each figure.
102
103 o start_y_pos - The y position on the page to start drawing at.
104
105 o y_pos_change - The change in y position between each figure.
106
107 o num_y_drawings - The number of drawings we'll have in the y
108 (up/down) direction.
109 """
110 for y_drawing in range(int(num_y_drawings)):
111
112
113 if ((y_drawing + 1) * self.number_of_columns >
114 len(self.distributions)):
115 num_x_drawings = len(self.distributions) - \
116 y_drawing * self.number_of_columns
117 else:
118 num_x_drawings = self.number_of_columns
119 for x_drawing in range(num_x_drawings):
120 dist_num = y_drawing * self.number_of_columns + x_drawing
121 cur_distribution = self.distributions[dist_num]
122
123
124 x_pos = start_x_pos + x_drawing * x_pos_change
125 end_x_pos = x_pos + x_pos_change
126 end_y_pos = start_y_pos - y_drawing * y_pos_change
127 y_pos = end_y_pos - y_pos_change
128
129
130 cur_distribution.draw(cur_drawing, x_pos, y_pos, end_x_pos,
131 end_y_pos)
132
133 - def _draw_legend(self, cur_drawing, start_y, width):
134 """Add a legend to the figure.
135
136 Subclasses can implement to provide a specialized legend.
137 """
138 pass
139
141 """Display the distribution of values as a bunch of bars.
142 """
144 """Initialize a Bar Chart display of distribution info.
145
146 Class attributes:
147
148 o display_info - the information to be displayed in the distribution.
149 This should be ordered as a list of lists, where each internal list
150 is a data set to display in the bar chart.
151 """
152 self.display_info = display_info
153
154 self.x_axis_title = ""
155 self.y_axis_title = ""
156 self.chart_title = ""
157 self.chart_title_size = 10
158
159 self.padding_percent = 0.15
160
161 - def draw(self, cur_drawing, start_x, start_y, end_x, end_y):
162 """Draw a bar chart with the info in the specified range.
163 """
164 bar_chart = VerticalBarChart()
165 if self.chart_title:
166 self._draw_title(cur_drawing, self.chart_title,
167 start_x, start_y, end_x, end_y)
168
169 x_start, x_end, y_start, y_end = \
170 self._determine_position(start_x, start_y, end_x, end_y)
171
172 bar_chart.x = x_start
173 bar_chart.y = y_start
174 bar_chart.width = abs(x_start - x_end)
175 bar_chart.height = abs(y_start - y_end)
176
177
178 bar_chart.data = self.display_info
179 bar_chart.valueAxis.valueMin = min(self.display_info[0])
180 bar_chart.valueAxis.valueMax = max(self.display_info[0])
181 for data_set in self.display_info[1:]:
182 if min(data_set) < bar_chart.valueAxis.valueMin:
183 bar_chart.valueAxis.valueMin = min(data_set)
184 if max(data_set) > bar_chart.valueAxis.valueMax:
185 bar_chart.valueAxis.valueMax = max(data_set)
186
187
188 if len(self.display_info) == 1:
189 bar_chart.groupSpacing = 0
190 style = TypedPropertyCollection(BarChartProperties)
191 style.strokeWidth = 0
192 style.strokeColor = colors.green
193 style[0].fillColor = colors.green
194
195 bar_chart.bars = style
196
197
198
199
200
201
202
203 cur_drawing.add(bar_chart)
204
205 - def _draw_title(self, cur_drawing, title, start_x, start_y, end_x, end_y):
206 """Add the title of the figure to the drawing.
207 """
208 x_center = start_x + (end_x - start_x) / 2
209 y_pos = end_y + (self.padding_percent * (start_y - end_y)) / 2
210 title_string = String(x_center, y_pos, title)
211 title_string.fontName = 'Helvetica-Bold'
212 title_string.fontSize = self.chart_title_size
213 title_string.textAnchor = "middle"
214
215 cur_drawing.add(title_string)
216
218 """Calculate the position of the chart with blank space.
219
220 This uses some padding around the chart, and takes into account
221 whether the chart has a title. It returns 4 values, which are,
222 in order, the x_start, x_end, y_start and y_end of the chart
223 itself.
224 """
225 x_padding = self.padding_percent * (end_x - start_x)
226 y_padding = self.padding_percent * (start_y - end_y)
227
228 new_x_start = start_x + x_padding
229 new_x_end = end_x - x_padding
230
231 if self.chart_title:
232 new_y_start = start_y - y_padding - self.chart_title_size
233 else:
234 new_y_start = start_y - y_padding
235
236 new_y_end = end_y + y_padding
237
238 return new_x_start, new_x_end, new_y_start, new_y_end
239
241 """Display the distribution of values as connected lines.
242
243 This distribution displays the change in values across the object as
244 lines. This also allows multiple distributions to be displayed on a
245 single graph.
246 """
249
250 - def draw(self, cur_drawing, start_x, start_y, end_x, end_y):
252