File Coverage

blib/lib/Chart/StackedBars.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             ## @file
2             # Implementation of Chart::StackedBars
3             #
4             # written by
5             # @author david bonner (dbonner@cs.bu.edu)
6             #
7             # maintained by the
8             # @author Chart Group at Geodetic Fundamental Station Wettzell (Chart@fs.wettzell.de)
9             # @date 2015-03-01
10             # @version 2.4.10
11             #
12              
13             ## @class Chart::StackedBars
14             # StackedBars class derived from class Base.
15             #
16             # This class provides all functions which are specific to
17             # stacked bars
18             package Chart::StackedBars;
19              
20 4     4   5715 use Chart::Base '2.4.10';
  0            
  0            
21             use GD;
22             use Carp;
23             use strict;
24              
25             @Chart::StackedBars::ISA = qw(Chart::Base);
26             $Chart::StackedBars::VERSION = '2.4.10';
27              
28             #>>>>>>>>>>>>>>>>>>>>>>>>>>#
29             # public methods go here #
30             #<<<<<<<<<<<<<<<<<<<<<<<<<<#
31              
32             #>>>>>>>>>>>>>>>>>>>>>>>>>>>#
33             # private methods go here #
34             #<<<<<<<<<<<<<<<<<<<<<<<<<<<#
35              
36             ## @fn private _check_data
37             # override check_data to make sure we don't get datasets with positive
38             # and negative values mixed
39             sub _check_data
40             {
41             my $self = shift;
42             my $data = $self->{'dataref'};
43             my $length = 0;
44             my ( $i, $j, $posneg );
45             my $composite;
46              
47             # remember the number of datasets
48             if ( defined $self->{'composite_info'} )
49             {
50             if ( $self->{'composite_info'}[0][0] =~ /^StackedBars$/i )
51             {
52             $composite = 0;
53             }
54             if ( $self->{'composite_info'}[1][0] =~ /^StackedBars$/i )
55             {
56             $composite = 1;
57             }
58              
59             # $self->{'num_datasets'} = $#{$data}; ###
60              
61             $self->{'num_datasets'} = ( $#{ $self->{'composite_info'}[$composite][1] } ) + 1;
62             }
63             else
64             {
65             $self->{'num_datasets'} = $#{$data};
66             }
67              
68             # remember the number of points in the largest dataset
69             $self->{'num_datapoints'} = 0;
70             for ( 0 .. $self->{'num_datasets'} )
71             {
72             if ( scalar( @{ $data->[$_] } ) > $self->{'num_datapoints'} )
73             {
74             $self->{'num_datapoints'} = scalar( @{ $data->[$_] } );
75             }
76             }
77              
78             # make sure the datasets don't mix pos and neg values
79             for $i ( 0 .. $self->{'num_datapoints'} - 1 )
80             {
81             $posneg = '';
82             for $j ( 1 .. $self->{'num_datasets'} )
83             {
84             if ( $data->[$j][$i] > 0 )
85             {
86             if ( $posneg eq 'neg' )
87             {
88             croak "The values for a Chart::StackedBars data point must either be all positive or all negative";
89             }
90             else
91             {
92             $posneg = 'pos';
93             }
94             }
95             elsif ( $data->[$j][$i] < 0 )
96             {
97             if ( $posneg eq 'pos' )
98             {
99             croak "The values for a Chart::StackedBars data point must either be all positive or all negative";
100             }
101             else
102             {
103             $posneg = 'neg';
104             }
105             }
106             }
107             }
108              
109             # find good min and max y-values for the plot
110             $self->_find_y_scale;
111              
112             # find the longest x-tick label
113             for ( @{ $data->[0] } )
114             {
115             if ( length($_) > $length )
116             {
117             $length = length($_);
118             }
119             }
120              
121             # now store it in the object
122             $self->{'x_tick_label_length'} = $length;
123              
124             return;
125             }
126              
127             ## @fn private _find_y_range
128             sub _find_y_range
129             {
130             my $self = shift;
131              
132             # This finds the minimum and maximum point-sum over all x points,
133             # where the point-sum is the sum of the dataset values for that point.
134             # If the y value in any dataset is undef for a given x, it simply
135             # adds nothing to the sum.
136              
137             my $data = $self->{'dataref'};
138             my $max = undef;
139             my $min = undef;
140             for my $i ( 0 .. $#{ $data->[0] } )
141             { # data point
142             my $sum = $data->[1]->[$i] || 0;
143             for my $dataset ( @$data[ 2 .. $#$data ] )
144             { # order not important
145             my $datum = $dataset->[$i];
146             $sum += $datum if defined $datum;
147             }
148             if ( defined $max )
149             {
150             if ( $sum > $max ) { $max = $sum }
151             elsif ( $sum < $min ) { $min = $sum }
152             }
153             else { $min = $max = $sum }
154             }
155              
156             # make sure all-positive or all-negative charts get anchored at
157             # zero so that we don't cut out some parts of the bars
158             if ( ( $max > 0 ) && ( $min > 0 ) )
159             {
160             $min = 0;
161             }
162             if ( ( $min < 0 ) && ( $max < 0 ) )
163             {
164             $max = 0;
165             }
166              
167             ( $min, $max );
168             }
169              
170             # ## override _find_y_scale to account for stacked bars
171             # sub _find_y_scale {
172             # my $self = shift;
173             # my $raw = $self->{'dataref'};
174             # my $data = [@{$raw->[1]}];
175             # my ($i, $j, $max, $min);
176             # my ($order, $mult, $tmp);
177             # my ($range, $delta, @dec, $y_ticks);
178             # my $labels = [];
179             # my $length = 0;
180             #
181             # # use realy weird max and min values
182             # $max = -999999999999;
183             # $min = 999999999999;
184             #
185             # # go through and stack them
186             # for $i (0..$self->{'num_datapoints'}-1) {
187             # for $j (2..$self->{'num_datasets'}) {
188             # $data->[$i] += $raw->[$j][$i];
189             # }
190             # }
191             #
192             # # get max and min values
193             # for $i (0..$self->{'num_datapoints'}-1) {
194             # if ($data->[$i] > $max) {
195             # $max = $data->[$i];
196             # }
197             # if ($data->[$i] < $min) {
198             # $min = $data->[$i];
199             # }
200             # }
201             #
202             # # make sure all-positive or all-negative charts get anchored at
203             # # zero so that we don't cut out some parts of the bars
204             # if (($max > 0) && ($min > 0)) {
205             # $min = 0;
206             # }
207             # if (($min < 0) && ($max < 0)) {
208             # $max = 0;
209             # }
210             #
211             # # calculate good max value
212             # if ($max < -10) {
213             # $tmp = -$max;
214             # $order = int((log $tmp) / (log 10));
215             # $mult = int ($tmp / (10 ** $order));
216             # $tmp = ($mult - 1) * (10 ** $order);
217             # $max = -$tmp;
218             # }
219             # elsif ($max < 0) {
220             # $max = 0;
221             # }
222             # elsif ($max > 10) {
223             # $order = int((log $max) / (log 10));
224             # $mult = int ($max / (10 ** $order));
225             # $max = ($mult + 1) * (10 ** $order);
226             # }
227             # elsif ($max >= 0) {
228             # $max = 10;
229             # }
230             #
231             # # now go for a good min
232             # if ($min < -10) {
233             # $tmp = -$min;
234             # $order = int((log $tmp) / (log 10));
235             # $mult = int ($tmp / (10 ** $order));
236             # $tmp = ($mult + 1) * (10 ** $order);
237             # $min = -$tmp;
238             # }
239             # elsif ($min < 0) {
240             # $min = -10;
241             # }
242             # elsif ($min > 10) {
243             # $order = int ((log $min) / (log 10));
244             # $mult = int ($min / (10 ** $order));
245             # $min = $mult * (10 ** $order);
246             # }
247             # elsif ($min >= 0) {
248             # $min = 0;
249             # }
250             #
251             # # put the appropriate min and max values into the object if necessary
252             # unless (defined ($self->{'max_val'})) {
253             # $self->{'max_val'} = $max;
254             # }
255             # unless (defined ($self->{'min_val'})) {
256             # $self->{'min_val'} = $min;
257             # }
258             #
259             # # generate the y_tick labels, store them in the object
260             # # figure out which one is going to be the longest
261             # $range = $self->{'max_val'} - $self->{'min_val'};
262             # $y_ticks = $self->{'y_ticks'} - 1;
263             # ## Don't adjust y_ticks if the user specified custom labels
264             # if ($self->{'integer_ticks_only'} =~ /^true$/i && ! $self->{'y_tick_labels'}) {
265             # unless (($range % $y_ticks) == 0) {
266             # while (($range % $y_ticks) != 0) {
267             # $y_ticks++;
268             # }
269             # $self->{'y_ticks'} = $y_ticks + 1;
270             # }
271             # }
272             #
273             # $delta = $range / $y_ticks;
274             # for (0..$y_ticks) {
275             # $tmp = $self->{'min_val'} + ($delta * $_);
276             # @dec = split /\./, $tmp;
277             # if ($dec[1] && (length($dec[1]) > 3)) {
278             # $tmp = sprintf("%.3f", $tmp);
279             # }
280             # $labels->[$_] = $tmp;
281             # if (length($tmp) > $length) {
282             # $length = length($tmp);
283             # }
284             # }
285             #
286             # # store it in the object
287             # $self->{'y_tick_labels'} = $labels;
288             # $self->{'y_tick_label_length'} = $length;
289             #
290             # # and return
291             # return;
292             # }
293              
294             ## @fn private _draw_data
295             # finally get around to plotting the data
296             sub _draw_data
297             {
298             my $self = shift;
299             my $raw = $self->{'dataref'};
300             my $data = [];
301             my $misccolor = $self->_color_role_to_index('misc');
302             my ( $width, $height, $delta, $map, $mod );
303             my ( $x1, $y1, $x2, $y2, $x3, $y3, $i, $j, $color, $cut );
304             my $pink = $self->{'gd_obj'}->colorAllocate( 255, 0, 255 );
305              
306             # init the imagemap data field if they want it
307             if ( $self->true( $self->{'imagemap'} ) )
308             {
309             $self->{'imagemap_data'} = [];
310             }
311              
312             # width and height of remaining area, delta for width of bars, mapping value
313             $width = $self->{'curr_x_max'} - $self->{'curr_x_min'};
314              
315             if ( $self->true( $self->{'spaced_bars'} ) )
316             {
317             $delta = ( $width / ( $self->{'num_datapoints'} * 2 ) );
318             }
319             else
320             {
321             $delta = $width / $self->{'num_datapoints'};
322             }
323             $height = $self->{'curr_y_max'} - $self->{'curr_y_min'};
324             $map = $height / ( $self->{'max_val'} - $self->{'min_val'} );
325              
326             # get the base x and y values
327             $x1 = $self->{'curr_x_min'};
328             if ( $self->{'min_val'} >= 0 )
329             {
330             $y1 = $self->{'curr_y_max'};
331             $mod = $self->{'min_val'};
332             }
333             elsif ( $self->{'max_val'} <= 0 )
334             {
335             $y1 = $self->{'curr_y_min'};
336             $mod = $self->{'max_val'};
337             }
338             else
339             {
340             $y1 = $self->{'curr_y_min'} + ( $map * $self->{'max_val'} );
341             $mod = 0;
342             $self->{'gd_obj'}->line( $self->{'curr_x_min'}, $y1, $self->{'curr_x_max'}, $y1, $misccolor );
343             }
344              
345             # create another copy of the data, but stacked
346             $data->[1] = [ @{ $raw->[1] } ];
347             for $i ( 0 .. $self->{'num_datapoints'} - 1 )
348             {
349             for $j ( 2 .. $self->{'num_datasets'} )
350             {
351             $data->[$j][$i] = $data->[ $j - 1 ][$i] + $raw->[$j][$i];
352             }
353             }
354              
355             # draw the damn bars
356             for $i ( 0 .. $self->{'num_datapoints'} - 1 )
357             {
358              
359             # init the y values for this datapoint
360             $y2 = $y1;
361              
362             for $j ( 1 .. $self->{'num_datasets'} )
363             {
364              
365             # get the color
366             $color = $self->_color_role_to_index( 'dataset' . ( $j - 1 ) );
367              
368             # set up the geometry for the bar
369             if ( $self->true( $self->{'spaced_bars'} ) )
370             {
371             $x2 = $x1 + ( 2 * $i * $delta ) + ( $delta / 2 );
372             $x3 = $x2 + $delta;
373              
374             }
375             else
376             {
377             $x2 = $x1 + ( $i * $delta );
378             $x3 = $x2 + $delta;
379             }
380             $y3 = $y1 - ( ( $data->[$j][$i] - $mod ) * $map );
381              
382             #cut the bars off, if needed
383             if ( $data->[$j][$i] > $self->{'max_val'} )
384             {
385             $y3 = $y1 - ( ( $self->{'max_val'} - $mod ) * $map );
386             $cut = 1;
387             }
388             elsif ( $data->[$j][$i] < $self->{'min_val'} )
389             {
390             $y3 = $y1 - ( ( $self->{'min_val'} - $mod ) * $map );
391             $cut = 1;
392             }
393             else
394             {
395             $cut = 0;
396             }
397              
398             # draw the bar
399             ## y2 and y3 are reversed in some cases because GD's fill
400             ## algorithm is lame
401             if ( $data->[$j][$i] > 0 )
402             {
403             $self->{'gd_obj'}->filledRectangle( $x2, $y3, $x3, $y2, $color );
404             if ( $self->true( $self->{'imagemap'} ) )
405             {
406             $self->{'imagemap_data'}->[$j][$i] = [ $x2, $y3, $x3, $y2 ];
407             }
408             }
409             else
410             {
411             $self->{'gd_obj'}->filledRectangle( $x2, $y2, $x3, $y3, $color );
412             if ( $self->true( $self->{'imagemap'} ) )
413             {
414             $self->{'imagemap_data'}->[$j][$i] = [ $x2, $y2, $x3, $y3 ];
415             }
416             }
417              
418             # now outline it. outline red if the bar had been cut off
419             unless ($cut)
420             {
421             $self->{'gd_obj'}->rectangle( $x2, $y2, $x3, $y3, $misccolor );
422             }
423             else
424             {
425             $self->{'gd_obj'}->rectangle( $x2, $y2, $x3, $y3, $misccolor );
426             $self->{'gd_obj'}->rectangle( $x2, $y1, $x3, $y3, $pink );
427             }
428              
429             # now bootstrap the y values
430             $y2 = $y3;
431             }
432             }
433              
434             # and finaly box it off
435             $self->{'gd_obj'}
436             ->rectangle( $self->{'curr_x_min'}, $self->{'curr_y_min'}, $self->{'curr_x_max'}, $self->{'curr_y_max'}, $misccolor );
437             return;
438              
439             }
440              
441             ## @fn private _draw_left_legend
442             sub _draw_left_legend
443             {
444             my $self = shift;
445             my @labels = @{ $self->{'legend_labels'} };
446             my ( $x1, $x2, $x3, $y1, $y2, $width, $color, $misccolor, $w, $h, $brush );
447             my $font = $self->{'legend_font'};
448              
449             # make sure we're using a real font
450             unless ( ( ref($font) ) eq 'GD::Font' )
451             {
452             croak "The subtitle font you specified isn\'t a GD Font object";
453             }
454              
455             # get the size of the font
456             ( $h, $w ) = ( $font->height, $font->width );
457              
458             # get the miscellaneous color
459             $misccolor = $self->_color_role_to_index('misc');
460              
461             # find out how wide the largest label is
462             $width =
463             ( 2 * $self->{'text_space'} ) +
464             ( $self->{'max_legend_label'} * $w ) +
465             $self->{'legend_example_size'} +
466             ( 2 * $self->{'legend_space'} );
467              
468             # get some base x-y coordinates
469             $x1 = $self->{'curr_x_min'};
470             $x2 = $self->{'curr_x_min'} + $width;
471             $y1 = $self->{'curr_y_min'} + $self->{'graph_border'};
472             $y2 =
473             $self->{'curr_y_min'} +
474             $self->{'graph_border'} +
475             $self->{'text_space'} +
476             ( $self->{'num_datasets'} * ( $h + $self->{'text_space'} ) ) +
477             ( 2 * $self->{'legend_space'} );
478              
479             # box the legend off
480             $self->{'gd_obj'}->rectangle( $x1, $y1, $x2, $y2, $misccolor );
481              
482             # leave that nice space inside the legend box
483             $x1 += $self->{'legend_space'};
484             $y1 += $self->{'legend_space'} + $self->{'text_space'};
485              
486             # now draw the actual legend
487             for ( 0 .. $#labels )
488             {
489              
490             # get the color
491             my $c = $self->{'num_datasets'} - $_ - 1;
492              
493             # color of the datasets in the legend
494             if ( $self->{'dataref'}[1][0] < 0 )
495             {
496             $color = $self->_color_role_to_index( 'dataset' . $_ );
497             }
498             else
499             {
500             $color = $self->_color_role_to_index( 'dataset' . $c );
501             }
502              
503             # find the x-y coords
504             $x2 = $x1;
505             $x3 = $x2 + $self->{'legend_example_size'};
506             $y2 = $y1 + ( $_ * ( $self->{'text_space'} + $h ) ) + $h / 2;
507              
508             # do the line first
509             $self->{'gd_obj'}->line( $x2, $y2, $x3, $y2, $color );
510              
511             # reset the brush for points
512             $brush = $self->_prepare_brush( $color, 'point', $self->{ 'pointStyle' . $_ } );
513             $self->{'gd_obj'}->setBrush($brush);
514              
515             # draw the point
516             $self->{'gd_obj'}->line( int( ( $x3 + $x2 ) / 2 ), $y2, int( ( $x3 + $x2 ) / 2 ), $y2, gdBrushed );
517              
518             # now the label
519             $x2 = $x3 + ( 2 * $self->{'text_space'} );
520             $y2 -= $h / 2;
521              
522             # order of the datasets in the legend
523             if ( $self->{'dataref'}[1][0] < 0 )
524             {
525             $self->{'gd_obj'}->string( $font, $x2, $y2, $labels[$_], $color );
526             }
527             else
528             {
529             $self->{'gd_obj'}->string( $font, $x2, $y2, $labels[$c], $color );
530             }
531             }
532              
533             # mark off the used space
534             $self->{'curr_x_min'} += $width;
535              
536             # and return
537             return 1;
538             }
539              
540             ## @fn private _draw_right_legend
541             sub _draw_right_legend
542             {
543             my $self = shift;
544             my @labels = @{ $self->{'legend_labels'} };
545             my ( $x1, $x2, $x3, $y1, $y2, $width, $color, $misccolor, $w, $h, $brush );
546             my $font = $self->{'legend_font'};
547              
548             # make sure we're using a real font
549             unless ( ( ref($font) ) eq 'GD::Font' )
550             {
551             croak "The subtitle font you specified isn\'t a GD Font object";
552             }
553              
554             # get the size of the font
555             ( $h, $w ) = ( $font->height, $font->width );
556              
557             # get the miscellaneous color
558             $misccolor = $self->_color_role_to_index('misc');
559              
560             # find out how wide the largest label is
561             $width =
562             ( 2 * $self->{'text_space'} ) +
563             ( $self->{'max_legend_label'} * $w ) +
564             $self->{'legend_example_size'} +
565             ( 2 * $self->{'legend_space'} );
566              
567             # get some starting x-y values
568             $x1 = $self->{'curr_x_max'} - $width;
569             $x2 = $self->{'curr_x_max'};
570             $y1 = $self->{'curr_y_min'} + $self->{'graph_border'};
571             $y2 =
572             $self->{'curr_y_min'} +
573             $self->{'graph_border'} +
574             $self->{'text_space'} +
575             ( $self->{'num_datasets'} * ( $h + $self->{'text_space'} ) ) +
576             ( 2 * $self->{'legend_space'} );
577              
578             # box the legend off
579             $self->{'gd_obj'}->rectangle( $x1, $y1, $x2, $y2, $misccolor );
580              
581             # leave that nice space inside the legend box
582             $x1 += $self->{'legend_space'};
583             $y1 += $self->{'legend_space'} + $self->{'text_space'};
584              
585             # now draw the actual legend
586             for ( 0 .. $#labels )
587             {
588              
589             # get the color
590             my $c = $self->{'num_datasets'} - $_ - 1;
591              
592             # color of the datasets in the legend
593              
594             if ( $self->{'dataref'}[1][0] < 0 )
595             {
596             $color = $self->_color_role_to_index( 'dataset' . $_ );
597             }
598             else
599             {
600             $color = $self->_color_role_to_index( 'dataset' . $c );
601             }
602              
603             # find the x-y coords
604             $x2 = $x1;
605             $x3 = $x2 + $self->{'legend_example_size'};
606             $y2 = $y1 + ( $_ * ( $self->{'text_space'} + $h ) ) + $h / 2;
607              
608             # do the line first
609             $self->{'gd_obj'}->line( $x2, $y2, $x3, $y2, $color );
610              
611             # reset the brush for points
612             $brush = $self->_prepare_brush( $color, 'point', $self->{ 'pointStyle' . $_ } );
613             $self->{'gd_obj'}->setBrush($brush);
614              
615             # draw the point
616             $self->{'gd_obj'}->line( int( ( $x3 + $x2 ) / 2 ), $y2, int( ( $x3 + $x2 ) / 2 ), $y2, gdBrushed );
617              
618             # now the label
619             $x2 = $x3 + ( 2 * $self->{'text_space'} );
620             $y2 -= $h / 2;
621              
622             # order of the datasets in the legend
623             if ( $self->{'dataref'}[1][0] < 0 )
624             {
625             $self->{'gd_obj'}->string( $font, $x2, $y2, $labels[$_], $color );
626             }
627             else
628             {
629             $self->{'gd_obj'}->string( $font, $x2, $y2, $labels[$c], $color );
630             }
631             }
632              
633             # mark off the used space
634             $self->{'curr_x_max'} -= $width;
635              
636             # and return
637             return 1;
638             }
639              
640             ## be a good module and return 1
641             1;