File Coverage

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