File Coverage

blib/lib/MassSpec/ViewSpectrum.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package MassSpec::ViewSpectrum;
2            
3 1     1   23598 use strict;
  1         2  
  1         39  
4 1     1   4 use warnings;
  1         2  
  1         28  
5            
6 1     1   5 use Carp;
  1         6  
  1         92  
7 1     1   478 use GD;
  0            
  0            
8             use GD::Graph::lines;
9             use GD::Graph::lines;
10             use GD::Graph::colour qw(:lists :colours);
11            
12             our @ISA = qw(GD::Graph::Error);
13            
14             our $VERSION = '0.08';
15            
16            
17             # Preloaded methods go here.
18            
19             # Pairs of pattern and their associated annotations category used in
20             # conjunction with the colormap. This is an array rather than a hash
21             # because we want to consider the patterns in a prescribed order.
22             my @defaultannotationsmatching = (
23             '^y$|^y[ -]| y$| y[ -]', 'y',
24             '^b$|^b[ -]| b$| b[ -]', 'b',
25             '[iI]nternal', 'internal',
26             '^$', 'none'
27             );
28            
29            
30             my %defaultcolormap = (
31             'y' => 'red',
32             'b' => 'blue',
33             'internal' => 'green',
34             'other' => 'dbrown',
35             'none' => 'black'
36             );
37            
38             my %fontmap = (
39             'tiny' => gdTinyFont,
40             'small' => gdSmallFont,
41             'medium' => gdMediumBoldFont,
42             'large' => gdLargeFont,
43             );
44            
45             my %Defaults = (
46             width => 500,
47             height => 500,
48             title => '',
49             linewidth => 2,
50             extranegativeheight => 0.1,
51             ylabeldelta => 4, # offset of annotations on y axis measured in pixels
52             xticknumber => 5, # of tick marks on X axis
53             xlabeldelta => 6, # pixels for offset of annotations
54             yaxismultiplier => 2.0, # a ratio, used to permit vertical room for peak annotations
55             outputformat => 'png',
56             peakfontsize => 'medium',
57             x_label => 'm/z',
58             y_label => 'Relative Intensity',
59             annotationsmatching => \@defaultannotationsmatching,
60             colormap => \%defaultcolormap,
61             );
62            
63             sub new (\@\@;\@\%\%) # (masses, intensities, [optional] annotations, annotations_matching, colormap)
64             {
65             my $type = shift;
66             my $self = {};
67            
68             bless $self,$type;
69            
70             my($massesRef, $intensitiesRef, $annotationsRef, $annotations_matchingRef, $colormapRef) = @_;
71            
72             # Initialise all relevant parameters to defaults
73             $self->initialise() or return;
74            
75             $self->{masses} = $massesRef;
76             $self->{intensities} = $intensitiesRef;
77             $self->{annotations} = $annotationsRef;
78             $self->{annotationsmatching} = $annotations_matchingRef if $annotations_matchingRef;
79             $self->{colormap} = $colormapRef if $colormapRef;
80            
81             return $self;
82             }
83            
84             sub initialise
85             {
86             my $self = shift;
87            
88             foreach (keys %Defaults)
89             {
90             $self->set($_, $Defaults{$_});
91             }
92            
93             return $self;
94             }
95            
96            
97             sub set {
98             my ($self, $key, $value) = @_;
99            
100             $self->{$key} = $value;
101             }
102            
103             sub plot
104             {
105             my $self = shift;
106            
107             my $minmass;
108             my $maxmass;
109             my $maxintensity;
110             my $minintensity;
111            
112             my @data_for_graph;
113            
114             my $i;
115             my $j = 0;
116             #
117             # We are playing a trick by intentionally alternating the real y data values
118             # with undefined values (and GD::Graph's skip_undef option), so that GD::Graph
119             # doesn't try to plot our actual data ... we need to perform this plotting ourselves.
120             #
121             for ($i = 0; $i <= $#{$self->{masses}}; $i++) {
122             my $mass = $self->{masses}[$i];
123             my $intensity = $self->{intensities}[$i];
124            
125             if ($i > 0) {
126             $data_for_graph[0][$j] = $mass;
127             $data_for_graph[1][$j] = undef;
128             $j++;
129             }
130             $data_for_graph[0][$j] = $mass;
131             $data_for_graph[1][$j] = $intensity;
132             $minmass = $mass unless defined $minmass and $minmass < $mass;
133             $maxmass = $mass unless defined $maxmass and $maxmass > $mass;
134             $minintensity = $intensity unless defined $minintensity and $minintensity < $intensity;
135             $maxintensity = $intensity unless defined $maxintensity and $maxintensity > $intensity;
136             $j++;
137             }
138            
139             #
140             # adjust the min and max masses slightly, since otherwise
141             # the min and max mass peaks will be obscured by the y axis and the graph's
142             # right boundary
143             #
144             # we also force the ticks multiples of 5
145             #
146             my $massdiff = $maxmass - $minmass;
147             my $tickconstraint = 5.0 * $self->{xticknumber};
148             $minmass = int(($minmass - 0.04 * $massdiff)/$tickconstraint) * $tickconstraint;
149             $maxmass = int(($maxmass + 0.04 * $massdiff)/$tickconstraint + 0.5) * $tickconstraint;
150             $minmass = 0 if $minmass < 0;
151            
152             # note that we permit negative intensities; this permits some
153             # interesting visualization capabilities
154             #
155             # extra vertical space is required to make the labels fit
156             $minintensity = 0 if $minintensity > 0;
157             $minintensity -= $self->{extranegativeheight} if $minintensity < 0;
158            
159             my $graph = GD::Graph::lines->new($self->{width},$self->{height});
160             $graph->{graph}->setThickness($self->{linewidth});
161             #
162             # It turns out that if we don't specify x_tick_number explicitly, it becomes
163             # quite messy to compute the conversion of x coordinates and requires the use
164             # of lots of undocumented GD::Graph internals
165             #
166             # We are playing a trick with skip_undef, so that GD::Graph doesn't try to plot
167             # our actual data ... we need to perform this plotting ourselves.
168             #
169             # We double the maximum intensity so as to leave (hopefully) enough vertical
170             # height for peak annotations
171             #
172             $graph->set(
173             title => $self->{title},
174             x_label => $self->{x_label},
175             x_label_position => 0.5,
176             skip_undef => 1,
177             x_tick_number => $self->{xticknumber},
178             x_min_value => $minmass,
179             x_max_value => $maxmass,
180             x_number_format => "%.1f",
181             y_number_format => "%.2f",
182             y_min_value => $minintensity * $self->{yaxismultiplier},
183             y_max_value => $maxintensity * $self->{yaxismultiplier},
184             y_label => $self->{y_label}) or die $graph->error;
185            
186             $graph->set_x_axis_font(gdLargeFont);
187             $graph->set_y_axis_font(gdLargeFont);
188            
189             #
190             # draw the axes and their labels, and subsequently use the computed geometry
191             # for scaling and translating our data points and annotations
192             #
193             my $im = $graph->plot(\@data_for_graph) or die $graph->error;
194            
195             my %colors;
196            
197             # make the background transparent and interlaced
198             $colors{'white'} = $im->colorAllocate(255,255,255);
199             $im->transparent($colors{'white'});
200             $im->interlaced('true');
201            
202             for ($i = 0; $i <= $#{$self->{masses}}; $i++) {
203             my $pattern;
204             my $colorname;
205             my $match = 'other';
206             my $annot = $self->{annotations}[$i];
207             my $mass = $self->{masses}[$i];
208             my $intensity = $self->{intensities}[$i];
209             my $discardThisAnnotation = 0;
210             my $patternIndex;
211            
212             # by convention, a leading @ means discard this annotation, but
213             # use it for purposes of coloring the peak
214             if ($annot =~ m/^@/) {
215             $discardThisAnnotation = 1;
216             $annot =~ s/^.//;
217             }
218            
219             PATTERN:
220             for ($patternIndex = 0; $patternIndex < scalar(@{$self->{annotationsmatching}}); $patternIndex += 2) {
221             $pattern = $self->{annotationsmatching}[$patternIndex];
222             if (defined($annot) && $pattern && $annot =~ m/$pattern/) {
223             $match = $self->{annotationsmatching}[$patternIndex + 1];
224             last PATTERN;
225             }
226             }
227            
228             $colorname = $self->{colormap}{$match};
229             _init_clr ($im, $colorname, \%colors) or carp "Unable to allocate color $colorname \n";
230             # print "match $match color $colorname \n";
231            
232             # draw vertical mass peaks and their annotations, if any
233             _myline($self,$graph,$im,$mass,0,$mass,$intensity,$colors{$colorname});
234            
235             my $gdfont = $fontmap{$self->{peakfontsize}};
236             $gdfont = gdMediumBoldFont unless $gdfont;
237            
238             # for negative values we label all mass peaks starting at the
239             # bottom of the graph, since we lack the capability to
240             # compute the vertical height of the labels and don't want
241             # to require TrueType font availability in order to use
242             # GD's stringFT method
243             unless ($discardThisAnnotation) {
244             if ($intensity >= 0) {
245             _myannot($graph,$im,$mass,$intensity,$annot,$colors{$colorname},$self->{xlabeldelta},$self->{ylabeldelta},$gdfont);
246             } else {
247             _myannot($graph,$im,$mass,$minintensity*$self->{yaxismultiplier}*0.95,$annot,$colors{$colorname},$self->{xlabeldelta},0,$gdfont)
248             }
249             }
250            
251             }
252            
253             return $im->gif if ($self->{outputformat} eq 'gif');
254             return $im->jpeg if ($self->{outputformat} eq 'jpeg');
255             return $im->png;
256            
257             }
258            
259             sub _myline {
260             my($self,$graph,$img,$xb,$yb,$xe,$ye,$color) = @_;
261             my($xb2,$yb2,$xe2,$ye2);
262            
263             ($xb2,$yb2) = $graph->val_to_pixel($xb,$yb,1);
264             ($xe2,$ye2) = $graph->val_to_pixel($xe,$ye,1);
265             $img->line($xb2,$yb2,$xe2,$ye2,$color);
266             $self->{_hotspots}{$xb} = ['line',$xb2,$yb2,$xe2,$ye2,$self->{linewidth}];
267             }
268            
269             sub _myannot {
270             my($graph,$img,$x,$y,$annot,$color,$xlabeldelta,$ylabeldelta,$font) = @_;
271             my($x2,$y2) = $graph->val_to_pixel($x,$y,1);
272            
273             $img->stringUp($font,$x2-$xlabeldelta,$y2-$ylabeldelta,$annot,$color);
274             }
275            
276             # swiped from set_clr in GD::Graph
277             sub _init_clr ($$\%) {
278             my ($gd, $colorname, $colorsRef) = @_;
279            
280             my @rgb = _rgb($colorname);
281             # All of this could potentially be done by using colorResolve
282             # The problem is that colorResolve doesn't return an error
283             # condition (-1) if it can't allocate a color. Instead it always
284             # returns 0.
285            
286             # Check if this colour already exists on the canvas
287             my $i = $gd->colorExact(@rgb);
288             # if not, allocate a new one, and return its index
289             $i = $gd->colorAllocate(@rgb) if $i < 0;
290             # if this fails, we should use colorClosest.
291             $i = $gd->colorClosest(@rgb) if $i < 0;
292            
293             $$colorsRef{$colorname} = $i unless $i < 0;
294            
295             return $i;
296            
297             }
298             1;
299            
300             __END__