File Coverage

blib/lib/GIFgraph/boxplot.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #==========================================================================
2             # Copyright (c) 1999 Nigel Wright
3             #--------------------------------------------------------------------------
4             #
5             # Name:
6             # GIFgraph::boxplot.pm
7             #
8             # Description:
9             # Module that extends GIFgraph capabilities to create
10             # box-and-whisker graphs.
11             #
12             #==========================================================================
13            
14             package GIFgraph::boxplot;
15              
16 1     1   2511 use strict qw(vars refs subs);
  1         3  
  1         34  
17 1     1   981 use Statistics::Descriptive;
  1         17917  
  1         33  
18 1     1   1554 use GIFgraph::axestype;
  0            
  0            
19             use GIFgraph::utils qw(:all);
20              
21             die "\nERROR: GIFgraph must be version 1.10 or greater.\n"
22             unless ($GIFgraph::VERSION ge '1.10');
23              
24             @GIFgraph::boxplot::ISA = qw( GIFgraph::axestype );
25             $GIFgraph::boxplot::VERSION = '1.00';
26              
27             my %Defaults = (
28             box_spacing => 10,
29             x_label_position => 1/2,
30             r_margin => 25,
31              
32             # do_stats default value is 1, meaning raw data is used for each box.
33             # the user can set it to 0, in which case they must put all data for each
34             # box in the following form:
35             # [mean, minimum, lower-pctile, median, upper-pctile, maximum]
36             do_stats => 1,
37              
38             # multiplied by the box height to determine the length of the whiskers
39             step_const => 1.5,
40              
41             # the percentage used to determing the box top and bottom
42             upper_percent => 75,
43             lower_percent => 25,
44            
45             # number of steps between the edge of the box and the point
46             # defining outliers vs far-out-values
47             fov_const => 1,
48            
49             # produces a warning in case their are not enough pixels to properly
50             # draw each box
51             # set to 1 to turn the possibilty for warning on, 0 to turn it off
52             spacing_warning => 1,
53            
54             # used for setting proper x,y position of symbol characters
55             symbol_font => undef,
56             font_offset => undef,
57            
58             # allows the user to turn off all warnings in case they do not want to
59             # receive print statements when using the program. default value is 1.
60             # 0 disables all warnings/suggestions
61             warnings => 1,
62              
63             # set to 0 to draw only the box outlines and symbols
64             box_fill => 1,
65              
66             # sets the symbol color to be used
67             # dblue is used as default to match the rest of GIFgraph defaults
68             symbolc => 'dblue',
69             );
70              
71             {
72             sub initialise()
73             {
74             my $self = shift;
75              
76             $self->SUPER::initialise();
77              
78             my $key;
79             foreach $key (keys %Defaults)
80             {
81             $self->set( $key => $Defaults{$key} );
82             }
83             }
84            
85             # PRIVATE
86             sub draw_data{
87              
88             my $s = shift;
89             my $g = shift;
90             my $d = shift;
91              
92             # draw the 'zero' axis
93             $g->line(
94             $s->{left}, $s->{zeropoint},
95             $s->{right}, $s->{zeropoint},
96             $s->{fgci} );
97              
98             # add in the boxplots
99             $s->SUPER::draw_data($g, $d);
100             }
101            
102             # draws the boxplots
103             sub draw_data_set($$$)
104             {
105             my $s = shift;
106             my $g = shift;
107             my $d = shift;
108             my $ds = shift;
109             my $box_s = _round($s->{box_spacing}/2);
110              
111             # Pick a fill colour for current data set
112             my $dsci = $s->set_clr( $g, $s->pick_data_clr($ds) );
113              
114             # symbol colour is set to the default value
115             my $medci = $s->set_clr( $g, GIFgraph::_rgb($s->{symbolc}) );
116              
117             for my $i (0 .. $s->{numpoints})
118             {
119             if ( $s->{do_stats} )
120             {
121             next unless (defined $d->[$i][0]);
122             }
123             else
124             {
125             for my $j (0..5)
126             { next unless (defined $d->[$i][$j]); }
127             }
128              
129             # variable declaration
130             my ($stat, $upper, $medianv, $lower, $meanv,
131             $step, $minim, $maxim, $highest, $lowest);
132            
133             if ( $s->{do_stats} )
134             {
135             # used for simple statistical calculations
136             $stat = Statistics::Descriptive::Full->new();
137              
138             # add all the data for each box
139             my $j; # declaration required for comparison below
140             for($j=0; defined $d->[$i][$j]; $j++)
141             {
142             $stat->add_data($d->[$i][$j]);
143             }
144            
145             # check for minimum number of data points within the
146             # current data set. 4 points are required for stats.
147             if ($j < 4)
148             {
149             if ( $s->{warnings} )
150             {
151             print "\nData set ", $i+1,
152             " does not contain the ",
153             "minimum of 4 data points.\n",
154             "It has been left blank.\n";
155             }
156             next;
157             }
158            
159             # get all the values needed for making the boxplot
160             $upper = $stat->percentile( $s->{upper_percent} );
161             $lower = $stat->percentile( $s->{lower_percent} );
162             $meanv = $stat->mean();
163             $medianv = $stat->median();
164             $step = $s->{step_const}*($upper-$lower);
165              
166             #find max and min data points that are within one step
167             if ($stat->max() < $upper+$step)
168             { $maxim = $stat->max(); }
169             else { $maxim = $upper+$step; }
170            
171             if ($stat->min() > $lower-$step)
172             { $minim = $stat->min(); }
173             else { $minim = $lower-$step; }
174             }
175             else #( !$s->{do_stats} )
176             {
177             # collect all the stats needed for making the boxplot
178             $highest = $d->[$i][5];
179             $upper = $d->[$i][4];
180             $medianv = $d->[$i][3];
181             $lower = $d->[$i][2];
182             $lowest = $d->[$i][1];
183             $meanv = $d->[$i][0];
184             $step = $s->{step_const}*($upper-$lower);
185            
186             if ($highest < $upper+$step)
187             { $maxim = $highest; }
188             else { $maxim = $upper+$step; }
189            
190             if ($lowest > $lower-$step)
191             { $minim = $lowest; }
192             else { $minim = $lower-$step; }
193             } # end of else
194              
195             my ($xp, $t, $max, $min, $mean, $median, $b);
196              
197             # get coordinates of top of box
198             ($xp, $t) = $s->val_to_pixel($i+1, $upper, $ds);
199              
200             # max
201             ($xp, $max) = $s->val_to_pixel($i+1, $maxim, $ds);
202              
203             # min
204             ($xp, $min) = $s->val_to_pixel($i+1, $minim, $ds);
205              
206             # mean
207             ($xp, $mean) = $s->val_to_pixel($i+1, $meanv, $ds);
208              
209             # median
210             ($xp, $median) = $s->val_to_pixel($i+1, $medianv, $ds);
211              
212             # calculate left and right of box
213             my $l = $xp
214             - _round($s->{x_step}/2)
215             + _round(($ds - 1) * $s->{x_step}/$s->{numsets})
216             + $box_s;
217             my $r = $xp
218             - _round($s->{x_step}/2)
219             + _round($ds * $s->{x_step}/$s->{numsets})
220             - $box_s;
221              
222             # bottom
223             ($xp, $b) = $s->val_to_pixel($i+1, $lower, $ds);
224            
225             # set the center x location
226             my $c = $l - _round( ($l-$r)/2 );
227              
228             # check to make sure the boxplots have enough pixels
229             # to be properly displayed (else issue a warning)
230             # only do so once for the entire program,
231             # and only if the user has not turned off the warning
232             if ( $r-$l < 2 && $s->{spacing_warning} == 1 && $s->{warnings} )
233             {
234             print "\nWarning: the image size may be too ",
235             "small to display the boxplots.",
236             "\nSuggested Action: increase 'gifx' ",
237             "or decrease 'box_spacing'.";
238             $s->{spacing_warning} = 0;
239             }
240              
241             # begin all the drawing
242              
243             # the box filling
244             $g->filledRectangle( $l, $t, $r, $b, $dsci) if ($s->{box_fill});
245              
246             # box outline
247             $g->rectangle( $l, $t, $r, $b, $medci );
248              
249             # upper line and whisker
250             $g->line($c, $t, $c, $max, $medci);
251             $g->line($l, $max, $r, $max, $medci);
252              
253             # lower line and whisker
254             $g->line($c, $b, $c, $min, $medci);
255             $g->line($l, $min, $r, $min, $medci);
256            
257             # draw the median horizontal line
258             $g->line($l, $median, $r, $median, $medci );
259              
260             # set the font to use for the '+', 'o', and '*' chars
261            
262             # check and only set the font the first time through
263             # this avoids the case where the box size is on the
264             # boarder between two different fonts, resulting in
265             # different data sets being given different fonts
266             # because of slight differences in pixel rounding.
267             # also set all of the x and y off-set for each char
268             # so their best center is at the correct (x,y) location
269              
270             unless ( $s->{symbol_font} )
271             {
272             if ($r-$l <= 20)
273             {
274             $s->{symbol_font} = GD::gdTinyFont;
275             $s->{font_offset} = [2,3,1,4,1,3];
276             }
277             elsif ($r-$l <= 35)
278             {
279             $s->{symbol_font} = GD::gdSmallFont;
280             $s->{font_offset} = [2,6,2,7,2,6];
281             }
282             else
283             {
284             $s->{symbol_font} = GD::gdLargeFont;
285             $s->{font_offset} = [3,8,3,9,3,8];
286             }
287             }
288              
289             # set the font
290             my $font = $s->{symbol_font};
291              
292             # set the offsets
293             my ($plusx, $plusy, $ox, $oy, $asterx, $astery) =
294             @{ $s->{font_offset} };
295            
296             # draw the mean using a character '+'
297             $g->string($font, $c-$plusx, $mean-$plusy, "+", $medci);
298            
299             # draw any outliers as an 'o' character (defined as points
300             # between 1 and fov_const steps from the nearest box boundary).
301             # also draw far out points as "*" character (points more than
302             # fov_const steps from the nearest box boundary)
303             if ( $s->{do_stats} )
304             {
305             # first check all the values above the box
306             if ($stat->max() > $maxim)
307             {
308             for(my $j; defined $d->[$i][$j]; $j++)
309             {
310             if ($d->[$i][$j] > $maxim)
311             {
312             if( $d->[$i][$j] <=
313             $maxim +
314             $s->{fov_const}*$step )
315             # it is an outlier, so draw an 'o'
316             {
317             my($x, $y) =
318             $s->val_to_pixel(
319             $i+1,
320             $d->[$i][$j],
321             $ds
322             );
323            
324             $g->string($font,
325             $c-$ox, $y-$oy,
326             "o", $medci
327             );
328             }
329             else # it is a far-out value '*'
330             {
331             my($x, $y) =
332             $s->val_to_pixel(
333             $i+1,
334             $d->[$i][$j],
335             $ds
336             );
337              
338             $g->string( $font,
339             $c-$asterx, $y-$astery,
340             "*", $medci
341             );
342             }
343             }
344             }
345             }
346              
347             # now repeat the same procedure for values below the box
348             if ($stat->min() < $minim)
349             {
350             for (my $j; defined $d->[$i][$j]; $j++)
351             {
352             if ($d->[$i][$j] < $minim)
353             {
354             if( $d->[$i][$j] >=
355             $minim -
356             $s->{fov_const}*$step )
357             # it is an outlier, so draw an 'o'
358             {
359             my($x, $y) = $s->val_to_pixel(
360             $i+1,
361             $d->[$i][$j],
362             $ds
363             );
364              
365             $g->string( $font,
366             $c-$ox, $y-$oy,
367             "o", $medci
368             );
369             }
370             else # it is a far-out value, draw '*'
371             {
372             my($x, $y) = $s->val_to_pixel(
373             $i+1,
374             $d->[$i][$j],
375             $ds
376             );
377            
378             $g->string($font,
379             $c-$asterx, $y-$astery,
380             "*", $medci
381             );
382             }
383             }
384             }
385             }
386             }
387             else # !$s->{do_stats}
388             {
389             # first check if the highest value is above the upper whisker
390             if ($highest > $maxim)
391             {
392             if ( $highest <= $maxim + $s->{fov_const}*$step )
393             # outlier, so draw an 'o'
394             {
395             my($x, $y) =
396             $s->val_to_pixel($i+1, $highest, $ds);
397            
398             $g->string($font, $c-$ox, $y-$oy, "o", $medci);
399             }
400             else
401             # far out value, so draw an '*'
402             {
403             my($x, $y) =
404             $s->val_to_pixel($i+1, $highest, $ds);
405              
406             $g->string( $font, $c-$asterx,
407             $y-$astery, "*", $medci);
408             }
409             }
410             # now check if the lowest value is below the lower whisker
411             if ($lowest < $minim)
412             {
413             if ( $lowest >= $minim - $s->{fov_const}*$step )
414             # outlier, so draw an 'o'
415             {
416             my($x, $y) =
417             $s->val_to_pixel($i+1, $lowest, $ds);
418            
419             $g->string($font, $c-$ox, $y-$oy, "o", $medci);
420             }
421             else
422             # far out value, so draw an '*'
423             {
424             my($x, $y) =
425             $s->val_to_pixel($i+1, $lowest, $ds);
426              
427             $g->string( $font, $c-$asterx,
428             $y-$astery, "*", $medci);
429             }
430             }
431             } #end of else
432             } # end of for
433             }
434              
435             # rewrite 'get_max_min_y_all' because, unlike the other graph types,
436             # boxplot takes arrays as data, rather than scalars.
437             # the min and max y are set just as in the other graph types,
438             # this just looks within each array, so as to compare all the scalars
439            
440             sub get_max_min_y_all($) # \@data
441             {
442             my $s = shift;
443             my $d = shift;
444              
445             my $max = undef;
446             my $min = undef;
447            
448             if( $s->{do_stats} )
449             {
450             for my $i ( 1 .. $s->{numsets} ) # 1 because x-labels are [0]
451             {
452             for my $j ( 0 .. $s->{numpoints} )
453             {
454             for (my $k; defined $d->[$i][$j][$k]; $k++ )
455             {
456             $max = $d->[$i][$j][$k]
457             if ($d->[$i][$j][$k] > $max);
458             $min = $d->[$i][$j][$k]
459             if ($d->[$i][$j][$k] < $min);
460             }
461             }
462             }
463             }
464             else # !$s->{do_stats}
465             {
466             for my $i ( 1 .. $s->{numsets} )
467             {
468             for my $j ( 0 .. $s->{numpoints} )
469             {
470             $max = $d->[$i][$j][5]
471             if (!defined $max || $d->[$i][$j][5] > $max);
472             $min = $d->[$i][$j][1]
473             if (!defined $min || $d->[$i][$j][1] < $min);
474             }
475             }
476             }
477              
478             # the +3 and -3 are to make sure their is room enough to draw the
479             # entirety of the symbols on the graph, as they otherwise
480             # may overlap the graph boarder
481             return ($max+3, $min-3);
482             }
483            
484             } # End of package GIFgraph::boxplot
485             1;
486              
487             __END__