#==================================================================== # Chart::HorizontalBars # # written by Chart-Group # # maintained by the Chart Group # Chart@wettzell.ifag.de # #--------------------------------------------------------------------- # History: #---------- # $RCSfile: HorizontalBars.pm,v $ $Revision: 1.2 $ $Date: 2003/02/14 14:04:40 $ # $Author: dassing $ # $Log: HorizontalBars.pm,v $ # Revision 1.2 2003/02/14 14:04:40 dassing # First setup # #==================================================================== package Chart::HorizontalBars; use Chart::Base '2.4.1'; use GD; use Carp; use strict; @Chart::HorizontalBars::ISA = qw(Chart::Base); $Chart::HorizontalBars::VERSION = '2.4.1'; #>>>>>>>>>>>>>>>>>>>>>>>>>># # public methods go here # #<<<<<<<<<<<<<<<<<<<<<<<<<<# #>>>>>>>>>>>>>>>>>>>>>>>>>>># # private methods go here # #<<<<<<<<<<<<<<<<<<<<<<<<<<<# #draw x_ticks and their labels sub _draw_x_ticks { my $self = shift; my $data = $self->{'dataref'}; my $font = $self->{'tick_label_font'}; my $textcolor = $self->_color_role_to_index('text'); my $misccolor = $self->_color_role_to_index('misc'); my ($h, $w, $x1, $y1, ,$y2, $x2, $delta, $width, $label); my @labels = @{$self->{'y_tick_labels'}}; $self->{'grid_data'}->{'x'} = []; #make sure we have a real font unless ((ref $font) eq 'GD::Font') { croak "The tick label font you specified isn't a GD font object"; } #get height and width of the font ($h, $w) = ($font->height, $font->width); #get the right x-value and width if ( $self->{'y_axes'} =~ /^right$/i ){ $x1 = $self->{'curr_x_min'}; $width = $self->{'curr_x_max'} - $x1 -$self->{'tick_len'} - $self->{'text_space'} - $w * $self->{'x_tick_label_length'}; } elsif ( $self->{'y_axes'} =~ /^both$/i) { $x1 = $self->{'curr_x_min'} + $self->{'text_space'} + $w* $self->{'x_tick_label_length'} + $self->{'tick_len'}; $width = $self->{'curr_x_max'} - $x1 - $self->{'tick_len'} - $self->{'text_space'} - $w * $self->{'x_tick_label_length'}; } else { $x1 = $self->{'curr_x_min'} + $self->{'text_space'} + $w* $self->{'x_tick_label_length'} + $self->{'tick_len'}; $width = $self->{'curr_x_max'} - $x1; } #get the delta value $delta = $width / ($self->{'y_ticks'} -1) ; #draw the labels $y2 =$y1; if ($self->{'x_ticks'} =~ /^normal/i ) { #just normal ticks #get the point for updating later $y1 = $self->{'curr_y_max'} - 2*$self->{'text_space'} -$h - $self->{'tick_len'}; #get the start point $y2 = $y1 + $self->{'tick_len'} + $self->{'text_space'}; for (0..$#labels){ $label = $self->{'y_tick_labels'}[$_]; $x2 = $x1 + ($delta * $_) - ($w* length( $label)/2) ; $self->{'gd_obj'}->string($font, $x2, $y2 , $label , $textcolor); } } elsif ($self->{'x_ticks'} =~ /^staggered/i ) { #staggered ticks #get the point for updating later $y1 = $self->{'curr_y_max'} - 3*$self->{'text_space'} - 2*$h - $self->{'tick_len'}; for (0..$#labels) { $label = $self->{'y_tick_labels'}[$_]; $x2 = $x1 + ($delta * $_) - ($w* length( $label)/2); unless ($_%2) { $y2 = $y1 + $self->{'text_space'} + $self->{'tick_len'}; $self->{'gd_obj'}->string($font, $x2, $y2 , $label, $textcolor); } else { $y2 = $y1 + $h + 2*$self->{'text_space'} + $self->{'tick_len'}; $self->{'gd_obj'}->string($font, $x2, $y2 , $label, $textcolor); } } } elsif ($self->{'x_ticks'} =~ /^vertical/i ) { #vertical ticks #get the point for updating later $y1 = $self->{'curr_y_max'} - 2*$self->{'text_space'} -$w* $self->{'y_tick_label_length'} - $self->{'tick_len'}; for (0..$#labels){ $label = $self->{'y_tick_labels'}[$_]; #get the start point $y2 = $y1 + $self->{'tick_len'} + $w* length($label) + $self->{'text_space'}; $x2 = $x1 + ($delta * $_) - ($h /2); $self->{'gd_obj'}->stringUp($font, $x2, $y2 , $label , $textcolor); } } else { carp "I don't understand the type of x-ticks you specified"; } #update the curr x and y max value $self->{'curr_y_max'} = $y1; $self->{'curr_x_max'} = $x1 + $width; #draw the ticks $y1 =$self->{'curr_y_max'}; $y2 =$self->{'curr_y_max'} + $self->{'tick_len'}; for(0..$#labels ) { $x2 = $x1 + ($delta * $_); $self->{'gd_obj'}->line($x2, $y1, $x2, $y2, $misccolor); if (($self->{'grid_lines'} =~ /^true$/i) or ($self->{'x_grid_lines'} =~ /^true$/i)) { $self->{'grid_data'}->{'x'}->[$_] = $x2; } } return 1; } sub _draw_y_ticks { my $self = shift; my $side = shift || 'left'; my $data = $self->{'dataref'}; my $font = $self->{'tick_label_font'}; my $textcolor = $self->_color_role_to_index ('text'); my $misccolor = $self->_color_role_to_index ('misc'); my ($h, $w, $x1, $x2, $y1, $y2); my ($width, $height, $delta); $self->{'grid_data'}->{'y'} =[]; #make sure that is a real font unless ((ref $font) eq 'GD::Font') { croak "The tick label font isn't a GD Font object!"; } #get the size of the font ($h, $w) = ($font->height, $font->width); #figure out, where to draw if ($side =~ /^right$/i) { #get the right startposition $x1 = $self->{'curr_x_max'}; $y1 = $self->{'curr_y_max'} - $h/2; #get the delta values $height = $self->{'curr_y_max'} - $self->{'curr_y_min'} ; $delta = ($height) / ($self->{'num_datapoints'} > 0 ? $self->{'num_datapoints'} : 1); $y1 -= ($delta/2 ); #look if skipping is desired if (!defined($self->{'skip_y_ticks'})) { $self->{'skip_y_ticks'} =1; } #draw the labels for(0.. int (($self->{'num_datapoints'} - 1) / $self->{'skip_y_ticks'})) { $y2 = $y1 - ($delta) * ($_ * $self->{'skip_y_ticks'}); $x2 = $x1 + $self->{'tick_len'} + $self->{'text_space'}; $self->{'gd_obj'}->string($font, $x2, $y2, $self->{f_y_tick}->($data->[0][$_*$self->{'skip_y_ticks'}]), $textcolor); } #draw the ticks $x1 = $self->{'curr_x_max'}; $x2 = $self->{'curr_x_max'} + $self->{'tick_len'}; $y1 += $h/2; for(0..($self->{'num_datapoints'} -1 / $self->{'skip_y_ticks'})) { $y2 = $y1 - ($delta * $_); $self->{'gd_obj'}->line($x1,$y2,$x2,$y2,$misccolor); if ($self->{'grid_lines'} =~ /^true$/i or $self->{'x_grid_lines'} =~ /^true$/i ) { $self->{'grid_data'}->{'y'}->[$_] = $y2; } } } elsif ($side =~ /^both$/i) { #get the right startposition $x1 = $self->{'curr_x_max'}; $y1 = $self->{'curr_y_max'} - $h/2; #get the delta values $height = $self->{'curr_y_max'} - $self->{'curr_y_min'} ; $delta = ($height) / ($self->{'num_datapoints'} > 0 ? $self->{'num_datapoints'} : 1); $y1 -= ($delta/2 ); #look if skipping is desired if (!defined($self->{'skip_y_ticks'})) { $self->{'skip_y_ticks'} =1; } #first draw the right labels for(0.. int (($self->{'num_datapoints'} - 1) / $self->{'skip_y_ticks'})) { $y2 = $y1 - ($delta) * ($_ * $self->{'skip_y_ticks'}); $x2 = $x1 + $self->{'tick_len'} + $self->{'text_space'}; $self->{'gd_obj'}->string($font, $x2, $y2, $self->{f_y_tick}->($data->[0][$_*$self->{'skip_y_ticks'}]), $textcolor); } #then draw the right ticks $x1 = $self->{'curr_x_max'}; $x2 = $self->{'curr_x_max'} + $self->{'tick_len'}; $y1 += $h/2; for(0..($self->{'num_datapoints'} -1 / $self->{'skip_y_ticks'})) { $y2 = $y1 - ($delta * $_); $self->{'gd_obj'}->line($x1,$y2,$x2,$y2,$misccolor); if ($self->{'grid_lines'} =~ /^true$/i or $self->{'x_grid_lines'} =~ /^true$/i ) { $self->{'grid_data'}->{'y'}->[$_] = $y2; } } #get the right startposition $x1 = $self->{'curr_x_min'} ; $y1 = $self->{'curr_y_max'} -$h/2 ; #get the delta values for positioning $height = $self->{'curr_y_max'} - $self->{'curr_y_min'} ; $delta = ($height) / ($self->{'num_datapoints'} > 0 ? $self->{'num_datapoints'} : 1); $y1 -= ($delta/2 ); #then draw the left labels for(0.. int (($self->{'num_datapoints'} - 1) / $self->{'skip_y_ticks'})) { $y2 = $y1 - ($delta) * ($_ * $self->{'skip_y_ticks'}); $x2 = $x1 - $w * length($self->{f_y_tick}->($data->[0][$_*$self->{'skip_y_ticks'}])) #print the Labels right-sided + $w * $self->{'x_tick_label_length'}; $self->{'gd_obj'}->string($font, $x2, $y2, $self->{f_y_tick}->($data->[0][$_*$self->{'skip_y_ticks'}]), $textcolor); } #update the curr_x_min val $self->{'curr_x_min'} = $x1 + $self->{'text_space'} + $w* $self->{'x_tick_label_length'} + $self->{'tick_len'}; #finally draw the left ticks $x1 = $self->{'curr_x_min'}; $x2 = $self->{'curr_x_min'} - $self->{'tick_len'}; $y1 += $h/2; for(0..($self->{'num_datapoints'} -1 / $self->{'skip_y_ticks'})) { $y2 = $y1 - ($delta * $_); $self->{'gd_obj'}->line($x1,$y2,$x2,$y2,$misccolor); if ($self->{'grid_lines'} =~ /^true$/i or $self->{'x_grid_lines'} =~ /^true$/i ) { $self->{'grid_data'}->{'y'}->[$_] = $y2; } } } else { #get the right startposition $x1 = $self->{'curr_x_min'} ; $y1 = $self->{'curr_y_max'} -$h/2 ; #get the delta values for positioning $height = $self->{'curr_y_max'} - $self->{'curr_y_min'} ; $delta = ($height) / ($self->{'num_datapoints'} > 0 ? $self->{'num_datapoints'} : 1); $y1 -= ($delta/2 ); if (!defined($self->{'skip_y_ticks'})) { $self->{'skip_y_ticks'} =1; } #draw the labels for(0.. int (($self->{'num_datapoints'} - 1) / $self->{'skip_y_ticks'})) { $y2 = $y1 - ($delta) * ($_ * $self->{'skip_y_ticks'}); $x2 = $x1 - $w * length($self->{f_y_tick}->($data->[0][$_*$self->{'skip_y_ticks'}])) #print the Labels right-sided + $w * $self->{'x_tick_label_length'}; $self->{'gd_obj'}->string($font, $x2, $y2, $self->{f_y_tick}->($data->[0][$_*$self->{'skip_y_ticks'}]), $textcolor); } #update the curr_x_min val $self->{'curr_x_min'} = $x1 + $self->{'text_space'} + $w* $self->{'x_tick_label_length'} + $self->{'tick_len'}; #draw the ticks $x1 = $self->{'curr_x_min'}; $x2 = $self->{'curr_x_min'} - $self->{'tick_len'}; $y1 += $h/2; for(0..($self->{'num_datapoints'} -1 / $self->{'skip_y_ticks'})) { $y2 = $y1 - ($delta * $_); $self->{'gd_obj'}->line($x1,$y2,$x2,$y2,$misccolor); if ($self->{'grid_lines'} =~ /^true$/i or $self->{'x_grid_lines'} =~ /^true$/i ) { $self->{'grid_data'}->{'y'}->[$_] = $y2; } } } #now return return 1; } #overwrite the find_y_scale function, only to get the right f_x_ticks !!!!! sub _find_y_scale { my $self = shift; # Predeclare vars. my ($d_min, $d_max); # Dataset min & max. my ($p_min, $p_max); # Plot min & max. my ($tickInterval, $tickCount); my @tickLabels; # List of labels for each tick. my $maxtickLabelLen = 0; # The length of the longest tick label. # Find the datatset minimum and maximum. ($d_min, $d_max) = $self->_find_y_range(); # Force the inclusion of zero if the user has requested it. if( $self->{'include_zero'} =~ m!^true$!i ) { if( ($d_min * $d_max) > 0 ) # If both are non zero and of the same sign. { if( $d_min > 0 ) # If the whole scale is positive. { $d_min = 0; } else # The scale is entirely negative. { $d_max = 0; } } } if ( $self->{'integer_ticks_only'} =~ /^\d$/ ) { if ( $self->{'integer_ticks_only'} == 1 ) { $self->{'integer_ticks_only'} = 'true'; } else { $self->{'integer_ticks_only'} = 'false'; } } if( $self->{'integer_ticks_only'} =~ m!^true$!i ) { # Allow the dataset range to be overidden by the user. # f_min/max are booleans which indicate that the min & max should not be modified. my $f_min = defined $self->{'min_val'}; $d_min = $self->{'min_val'} if $f_min; my $f_max = defined $self->{'max_val'}; $d_max = $self->{'max_val'} if $f_max; # Assert against the min is larger than the max. if( $d_min > $d_max ) { croak "The the specified 'min_val' & 'max_val' values are reversed (min > max: $d_min>$d_max)"; } # The user asked for integer ticks, force the limits to integers. # & work out the range directly. $p_min = $self->_round2Tick($d_min, 1, -1); $p_max = $self->_round2Tick($d_max, 1, 1); my $skip = $self->{skip_int_ticks}; $tickInterval = $skip; $tickCount = ($p_max - $p_min ) /$skip +1; # Now sort out an array of tick labels. for( my $labelNum = $p_min; $labelNum<=$p_max; $labelNum+=$tickInterval ) { my $labelText; if( defined $self->{f_x_tick} ) { # Is _default_f_tick function used? if ( $self->{f_x_tick} == \&_default_f_tick) { $labelText = sprintf("%d", $labelNum); } else { $labelText = $self->{f_x_tick}->($labelNum); } } else { $labelText = sprintf("%d", $labelNum); } #print "labelText = $labelText\n"; push @tickLabels, $labelText; $maxtickLabelLen = length $labelText if $maxtickLabelLen < length $labelText; } } else { # Allow the dataset range to be overidden by the user. # f_min/max are booleans which indicate that the min & max should not be modified. my $f_min = defined $self->{'min_val'}; $d_min = $self->{'min_val'} if $f_min; my $f_max = defined $self->{'max_val'}; $d_max = $self->{'max_val'} if $f_max; # Assert against the min is larger than the max. if( $d_min > $d_max ) { croak "The the specified 'min_val' & 'max_val' values are reversed (min > max: $d_min>$d_max)"; } # Calculate the width of the dataset. (posibly modified by the user) my $d_width = $d_max - $d_min; # If the width of the range is zero, forcibly widen it # (to avoid division by zero errors elsewhere in the code). if( 0 == $d_width ) { $d_min--; $d_max++; $d_width = 2; } # Descale the range by converting the dataset width into # a floating point exponent & mantisa pair. my( $rangeExponent, $rangeMantisa ) = $self->_sepFP( $d_width ); my $rangeMuliplier = 10 ** $rangeExponent; # Find what tick # to use & how many ticks to plot, # round the plot min & max to suatable round numbers. ($tickInterval, $tickCount, $p_min, $p_max) = $self->_calcTickInterval($d_min/$rangeMuliplier, $d_max/$rangeMuliplier, $f_min, $f_max, $self->{'min_y_ticks'}, $self->{'max_y_ticks'}); # Restore the tickInterval etc to the correct scale $_ *= $rangeMuliplier foreach($tickInterval, $p_min, $p_max); #get teh precision for the labels my $precision = $self->{'precision'}; # Now sort out an array of tick labels. for( my $labelNum = $p_min; $labelNum<=$p_max; $labelNum+=$tickInterval ) { my $labelText; if( defined $self->{f_x_tick} ) { # Is _default_f_tick function used? if ( $self->{f_x_tick} == \&_default_f_tick) { $labelText = sprintf("%.".$precision."f", $labelNum); } else { $labelText = $self->{f_x_tick}->($labelNum); } } else { $labelText = sprintf("%.".$precision."f", $labelNum); } #print "labelText = $labelText\n"; push @tickLabels, $labelText; $maxtickLabelLen = length $labelText if $maxtickLabelLen < length $labelText; } } # Store the calculated data. $self->{'min_val'} = $p_min; $self->{'max_val'} = $p_max; $self->{'y_ticks'} = $tickCount; $self->{'y_tick_labels'} = \@tickLabels; $self->{'y_tick_label_length'} = $maxtickLabelLen; # and return. return 1; } ## finally get around to plotting the data sub _draw_data { my $self = shift; my $data = $self->{'dataref'}; my $misccolor = $self->_color_role_to_index('misc'); my ($x1, $x2, $x3, $y1, $y2, $y3); my $cut = 0; my ($width, $height, $delta1, $delta2, $map, $mod, $pink); my ($i, $j, $color); # init the imagemap data field if they wanted it if ($self->{'imagemap'} =~ /^true$/i) { $self->{'imagemap_data'} = []; } # find both delta values ($delta1 for stepping between different # datapoint names, $delta2 for setpping between datasets for that # point) and the mapping constant $width = $self->{'curr_x_max'} - $self->{'curr_x_min'}; $height = $self->{'curr_y_max'} - $self->{'curr_y_min'}; $delta1 = $height / ($self->{'num_datapoints'} > 0 ? $self->{'num_datapoints'} : 1); $map = $width / ($self->{'max_val'} - $self->{'min_val'}); if ($self->{'spaced_bars'} =~ /^true$/i) { $delta2 = $delta1 / ($self->{'num_datasets'} + 2); } else { $delta2 = $delta1 / $self->{'num_datasets'}; } # get the base x-y values $y1 = $self->{'curr_y_max'} - $delta2; if ($self->{'min_val'} >= 0) { $x1 = $self->{'curr_x_min'} ; $mod = $self->{'min_val'}; } elsif ($self->{'max_val'} <= 0) { $x1 = $self->{'curr_x_max'}; $mod = $self->{'max_val'}; } else { $x1 = $self->{'curr_x_min'} + abs($map * $self->{'min_val'}); $mod = 0; $self->{'gd_obj'}->line ($x1, $self->{'curr_y_min'}, $x1, $self->{'curr_y_max'}, $misccolor); } # draw the bars for $i (1..$self->{'num_datasets'}) { # get the color for this dataset $color = $self->_color_role_to_index('dataset'.($i-1)); # draw every bar for this dataset for $j (0..$self->{'num_datapoints'}) { # don't try to draw anything if there's no data if (defined ($data->[$i][$j])) { # find the bounds of the rectangle if ($self->{'spaced_bars'} =~ /^true$/i) { $y2 = $y1 - ($j * $delta1) - ($self->{'num_datasets'} * $delta2) + (($i-1) * $delta2); } else { $y2 = $y1 - ($j * $delta1) - ($self->{'num_datasets'} * $delta2) + (($i) * $delta2); } $x2 = $x1; $y3 = $y2 + $delta2; #cut the bars off, if needed if ($data->[$i][$j] > $self->{'max_val'}) { $x3 = $x1 + (($self->{'max_val'} - $mod ) * $map) -1; $cut = 1; } elsif ($data->[$i][$j] < $self->{'min_val'}) { $x3 = $x1 + (($self->{'min_val'} - $mod ) * $map) +1; $cut = 1; } else { $x3 = $x1 + (($data->[$i][$j] - $mod) * $map); $cut = 0; } # draw the bar ## y2 and y3 are reversed in some cases because GD's fill ## algorithm is lame if ($data->[$i][$j] < 0) { $self->{'gd_obj'}->filledRectangle ($x3, $y2, $x2, $y3, $color); if ($self->{'imagemap'} =~ /^true$/i) { $self->{'imagemap_data'}->[$i][$j] = [$x3, $y2, $x2, $y3]; } $self->{'gd_obj'}->filledRectangle ($x3, $y2, $x2, $y3, $color); if ($self->{'imagemap'} =~ /^true$/i) { $self->{'imagemap_data'}->[$i][$j] = [$x3, $y2, $x2, $y3]; } } else { $self->{'gd_obj'}->filledRectangle ($x2, $y2, $x3, $y3, $color); if ($self->{'imagemap'} =~ /^true$/i) { $self->{'imagemap_data'}->[$i][$j] = [$x2, $y2, $x3, $y3]; } } # now outline it. outline red if the bar had been cut off unless ($cut){ $self->{'gd_obj'}->rectangle ($x2, $y3, $x3, $y2, $misccolor); } else { $pink = $self->{'gd_obj'}->colorAllocate(255,0,255); $self->{'gd_obj'}->rectangle ($x2, $y3, $x3, $y2, $pink); } } else { if ($self->{'imagemap'} =~ /^true$/i) { $self->{'imagemap_data'}->[$i][$j] = [undef(), undef(), undef(), undef()]; } } } } # and finaly box it off $self->{'gd_obj'}->rectangle ($self->{'curr_x_min'}, $self->{'curr_y_min'}, $self->{'curr_x_max'}, $self->{'curr_y_max'}, $misccolor); return; } ## be a good module and return 1 1;