## @file
# Implementation of Chart::HorizontalBars
#
# maintained and written by the
# @author Chart Group at Geodetic Fundamental Station Wettzell (Chart@fs.wettzell.de)
# @date 2012-10-03
# @version 2.4.6
## @class Chart::HorizontalBars
# HorizontalBars class derived from class Base.
#
# This class provides all functions which are specific to
# horizontal bars
#
package Chart::HorizontalBars;
use Chart::Base '2.4.6';
use GD;
use Carp;
use strict;
@Chart::HorizontalBars::ISA = qw(Chart::Base);
$Chart::HorizontalBars::VERSION = '2.4.6';
#>>>>>>>>>>>>>>>>>>>>>>>>>>#
# public methods go here #
#<<<<<<<<<<<<<<<<<<<<<<<<<<#
#>>>>>>>>>>>>>>>>>>>>>>>>>>>#
# private methods go here #
#<<<<<<<<<<<<<<<<<<<<<<<<<<<#
## @method private int _draw_x_ticks()
# draw the x-ticks and their labels
# Overwrites this function of Chart::Base
# @return status
#
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->true( $self->{'grid_lines'} )
or $self->true( $self->{'x_grid_lines'} ) )
{
$self->{'grid_data'}->{'x'}->[$_] = $x2;
}
}
return 1;
}
## @fn private int _draw_y_ticks()
# draw the y-ticks and their labels
# Overwrites this function of Chart::Base
# @return status
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->true( $self->{'grid_lines'} )
or $self->true( $self->{'x_grid_lines'} ) )
{
$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->true( $self->{'grid_lines'} )
or $self->true( $self->{'x_grid_lines'} ) )
{
$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->true( $self->{'grid_lines'} )
or $self->true( $self->{'x_grid_lines'} ) )
{
$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->true( $self->{'grid_lines'} )
or $self->true( $self->{'x_grid_lines'} ) )
{
$self->{'grid_data'}->{'y'}->[$_] = $y2;
}
}
}
#now return
return 1;
}
## @fn private int _find_y_scale()
# find good values for the minimum and maximum y-value on the chart
# overwrite the find_y_scale function, only to get the right f_x_ticks !!!!!
# @return status
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->true( $self->{'include_zero'} ) )
{
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->true( $self->{'integer_ticks_only'} ) )
{
# 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;
}
## @fn private _draw_data
# finally get around to plotting the data for (horizontal) bars
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->true( $self->{'imagemap'} ) )
{
$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->true( $self->{'spaced_bars'} ) )
{
$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->true( $self->{'spaced_bars'} ) )
{
$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->true( $self->{'imagemap'} ) )
{
$self->{'imagemap_data'}->[$i][$j] = [ $x3, $y2, $x2, $y3 ];
}
$self->{'gd_obj'}->filledRectangle( $x3, $y2, $x2, $y3, $color );
if ( $self->true( $self->{'imagemap'} ) )
{
$self->{'imagemap_data'}->[$i][$j] = [ $x3, $y2, $x2, $y3 ];
}
}
else
{
$self->{'gd_obj'}->filledRectangle( $x2, $y2, $x3, $y3, $color );
if ( $self->true( $self->{'imagemap'} ) )
{
$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->true( $self->{'imagemap'} ) )
{
$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;