The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#===========================#
#                           #
#  Chart::Base              #
#  written by david bonner  #
#  dbonner@cs.bu.edu        #
#                           #
#===========================#


package Chart::Base;

use Carp;
use GD;
use strict;


#==================#
#  public methods  #
#==================#

sub new {
    my $class = shift;
    my $self = {};
    
    bless $self, $class;
    $self->my_init (@_);

    return $self;
}

sub set {
    my $obj = shift;
    my %hash = @_;

    for (keys (%hash)) {
	    $obj->{$_} = $hash{$_};
    }
}

sub add_pt {
    my $obj = shift;
    my @data = @_;   
    my $i = 0;
 
    if ($obj->{'data'} && $#_ != $#{$obj->{'data'}}) {
	carp ("New points must have a value for each dataset");
	return undef;
    }
    else {
	for $i (0..$#data) {
	    push @{$obj->{'data'}->[$i]}, $data[$i];
	}
	return 1;
    }
}

sub add_dataset {
    my $obj = shift;
    my ($set, $i);
    
    if ($obj->{'data'} && $#_ != $#{$obj->{'data'}->[0]}) {
	carp ("New datasets must have as many points as the current ones");
	return undef;
    }
    else {
	$set = $#{$obj->{'data'}} + 1;
	for $i (0..$#_) {
	    push @{$obj->{'data'}->[$set]}, $_[$i];
	}
	return 1;
    }
}

sub clear_data {
    my $obj = shift;
    
    undef $obj->{'data'};
}

sub get_data {
    my $obj = shift;
    
    return $obj->{'data'};
}

sub gif {
    my $obj = shift;
    my $file = shift;
    my $dataref = shift;
    my $prev_data;
    
    $prev_data = $obj->copy_data ($dataref);
    if ($prev_data == 1) {
	if ($#{$dataref} < 1) {
	    croak "Chart::* needs an array of labels and at least one array of data";
	}
	if ($#{$dataref->[0]} == 0) {
	    croak "There aren't any data points!";
	}
    }
	
    $obj->my_plot;
    
    open (GIF, ">$file") or croak ("Couldn\'t open $file:  $!");
    print GIF $obj->{'im'}->gif;
    close GIF;
}

sub cgi_gif {
    my $obj = shift;
    my $dataref = shift;
    my $prev_data;
    
    $prev_data = $obj->copy_data ($dataref);
    if ($prev_data == 1) {
	if ($#{$dataref} < 1) {
	    croak "Chart::* needs an array of labels and at least one array of data";
	}
	if ($#{$dataref->[0]} == 0) {
	    croak "There aren't any data points!";
	}
    }
    
    $obj->my_plot;
    
    print "Content-type: image/gif\n\n";
    print $obj->{'im'}->gif;
}

#===================#
#  private methods  #
#===================#

sub my_init {
    my $self = shift;
    
    #  gimme that image  
    if ($#_ == 1) {
	$self->{'im'} = new GD::Image($_[0], $_[1]);
	$self->{'x_min'} = 0;
	$self->{'x_max'} = $_[0];
	$self->{'y_min'} = 0;
	$self->{'y_max'} = $_[1];
    }
    else {
	$self->{'im'} = new GD::Image(400,300);
	$self->{'x_min'} = 0;
	$self->{'x_max'} = 400;
	$self->{'y_min'} = 0;
	$self->{'y_max'} = 300;
    }
    

    #  allocate some colors
    $self->set_colors;

    #  set the image to be interlaced
    $self->{'im'}->interlaced('true');

    #  tick length of 4 pixels
    $self->{'tick_len'} = 4;

    #  gimme 5 y ticks
    $self->{'y_ticks'} = 5;

    #  show me the legend
    $self->{'legend'} = 'true';

    #  stagger those x-tick labels
    $self->{'stagger_x_labels'} = 'true';

    #  set the pareto cutoff to be 5
    $self->{'cutoff'} = 5;

    #  set the point size to a 5 pixel square
    $self->{'pt_size'} = 4;

    #  give me a 10 pixel border around the whole thing
    $self->{'gif_border'} = 10;

    #  give me a 10 pixel border between the labels and the graph
    $self->{'graph_border'} = 10;

    #  a little space for the text
    $self->{'text_space'} = 2;

    #  pesky pareto graph needs to default sort
    $self->{'sort'} = ['desc', 1, 'num'] if (ref ($self) eq 'Chart::Pareto');
}

sub set_colors {
    my $self = shift;

    $self->{'im'}->colorAllocate (250, 250, 250);
    $self->{'im'}->colorAllocate (0, 0, 0);
    $self->{'im'}->colorAllocate (225, 0, 0);
    $self->{'im'}->colorAllocate (0, 225, 0);
    $self->{'im'}->colorAllocate (0, 0, 225);
    $self->{'im'}->colorAllocate (200, 0, 200);
    $self->{'im'}->colorAllocate (0, 200, 200);
    $self->{'im'}->colorAllocate (225, 225, 0);
    $self->{'im'}->colorAllocate (250, 170, 85);
    $self->{'im'}->colorAllocate (200,200,200);
}

sub copy_data {
    my $obj = shift;
    my $their_ref = shift;
    my $my_ref = [];
    my ($i, $j);

    if ($obj->{'data'}) {
	return -1;
    }
    else {
	for $i (0..$#{$their_ref}) {
	    for $j (0..$#{$their_ref->[$i]}) {
		$my_ref->[$i][$j] = $their_ref->[$i][$j];
	    }
	}
	$obj->{'data'} = $my_ref;
	return 1;
    }
}

sub my_plot {
    my $obj = shift;
    my $dataref = $obj->{'data'};

    if ($obj->{'colors'}) { $obj->set_user_colors }
    if ($obj->{'transparent'} && $obj->{'transparent'} eq 'true') { 
	my $white = $obj->get_color ('white');
	$obj->{'im'}->transparent ($white);
    }
    
    $obj->{'x_min'} += $obj->{'gif_border'};
    $obj->{'y_min'} += $obj->{'gif_border'};
    $obj->{'x_max'} -= $obj->{'gif_border'};
    $obj->{'y_max'} -= $obj->{'gif_border'};

    $obj->check_data; 

    if ($obj->{'title'}) { $obj->draw_title; }
    if ($obj->{'sub_title'}) { $obj->draw_sub_title; }
    if ($obj->{'legend'} eq 'true') { $obj->draw_legend ($dataref) }
    if ($obj->{'x_label'} or $obj->{'y_label'}) { $obj->draw_labels; }

    $obj->{'x_min'} += $obj->{'graph_border'};
    $obj->{'y_min'} += $obj->{'graph_border'};
    $obj->{'x_max'} -= $obj->{'graph_border'};
    $obj->{'y_max'} -= $obj->{'graph_border'};


    if ($obj->{'sort'}) { $obj->sort_data; } 
    $obj->draw_data;
}

sub check_data {
    my $obj = shift;
    my $ref = $obj->{'data'};
    my $mismatch;

    CHECK: for (1..$#{$ref}) {
	       if ($#{$ref->[$_]} > $#{$ref->[0]}) {
		   $mismatch = 1;
		   last CHECK;
	       }
    }

    if ($mismatch) {
	croak ("One or more data sets longer than set of data point labels");
    }
}
    
sub draw_title {
    my $obj = shift;
    my ($w, $h) = (gdLargeFont->width,gdLargeFont->height);
    my $black = $obj->get_color ('black');
    my ($x, $y);
    
    $y = $obj->{'y_min'} + $obj->{'text_space'};
    $obj->{'y_min'} += $h + 2 * $obj->{'text_space'} + $obj->{'gif_border'} / 2;
    $x = ((($obj->{'x_max'} - $obj->{'x_min'}) / $obj->{'text_space'}) - 
	  (($w * length ($obj->{'title'})) / $obj->{'text_space'}));
    $obj->{'im'}->string (gdLargeFont, $x, $y, $obj->{'title'}, $black);
} 

sub draw_sub_title {
    my $obj = shift;
    my ($w, $h) = (gdLargeFont->width,gdLargeFont->height);
    my $black = $obj->get_color ('black');
    my ($x, $y);
		        
    $y = $obj->{'y_min'} + $obj->{'text_space'};
    $obj->{'y_min'} += $h + 2 * $obj->{'text_space'} + $obj->{'gif_border'} / 2;
    $x = ((($obj->{'x_max'} - $obj->{'x_min'}) / $obj->{'text_space'}) -
          (($w * length ($obj->{'sub_title'})) / $obj->{'text_space'}));
    $obj->{'im'}->string (gdLargeFont, $x, $y, $obj->{'sub_title'}, $black);
}


sub draw_labels {
    my $obj = shift;
    my ($w, $h) = (gdMediumBoldFont->width,gdMediumBoldFont->height);
    my $black = $obj->get_color ('black');
    my ($x, $y);
    
    if ($obj->{'x_label'}) {
	$y = $obj->{'y_max'} - ($obj->{'text_space'} + $h);
	$x = (($obj->{'x_max'} - $obj->{'x_min'}) / 2) + $obj->{'x_min'}
	        - (length ($obj->{'x_label'}) / 2) * $w;
	$obj->{'im'}->string (gdMediumBoldFont, $x, $y, 
			      $obj->{'x_label'}, $black);
    }
    
    if ($obj->{'y_label'}) {
	$y = (($obj->{'y_max'} - $obj->{'y_min'}) / 2) + $obj->{'y_min'} +
	    (length ($obj->{'y_label'}) / 2) * $w;
	$x = $obj->{'x_min'} + $obj->{'text_space'};
	$obj->{'im'}->stringUp (gdMediumBoldFont, $x, $y, 
				$obj->{'y_label'}, $black);
    }

    $obj->{'y_max'} -= ($obj->{'x_label'}) ? $h + 2 * $obj->{'text_space'} : 0;
    $obj->{'x_min'} += ($obj->{'y_label'}) ? $h + 2 * $obj->{'text_space'} : 0;
}

sub draw_legend {
    my $obj = shift;
    my $dataref = $obj->{'data'};
    my (@labels, $legend_w, $legend_h, $color, $dash, $ymin);
    my ($w, $h) = (gdSmallFont->width, gdSmallFont->height);
    my $black = $obj->get_color ('black');
    my $max_len = 0;

    #==========================#
    #  prepare list of labels  #
    #==========================#

    if ($obj->{'legend_labels'}) {
	@labels = @{$obj->{'legend_labels'}};
	if ($#labels != $#{$dataref} - 1) {
	    croak ("Number of data set labels does not match number of data sets");
	}
    }
    else {
	for (1..$#{$dataref}) {
	    $labels[$_-1] = "Dataset $_";
	}
    }

    for (@labels) {
	my $str_len = length ($_);
	if ($str_len > $max_len) {
	    $max_len = $str_len;
	}
    }

    #===============#
    #  draw legend  #
    #===============#

    $ymin = $obj->{'y_min'} + $obj->{'graph_border'};

    if (!($obj->{'dashed_lines'})) {
	$legend_h = ($#labels + 1) * ($h + 2 * $obj->{'text_space'});
	$legend_w = ($max_len * $w) + 3 * $obj->{'text_space'};
	$obj->{'x_max'} -= $legend_w + 2 * $obj->{'text_space'};
	
	$obj->{'im'}->rectangle ($obj->{'x_max'} + 2 * $obj->{'text_space'},
				 $ymin,
				 $obj->{'x_max'} + 2 * $obj->{'text_space'} 
			             + $legend_w,
				 $ymin + $legend_h,
				 $black);
	
	for (0..$#labels) {
	    $color = $obj->data_color($_);
	    
	    $obj->{'im'}->string (gdSmallFont,
				  $obj->{'x_max'} + 7,
				  $ymin + $obj->{'text_space'} 
			              + $_ * ($h + 2 * $obj->{'text_space'}),
				  $labels[$_],
				  $color);
	}
    }
    else {
	$legend_h = ($#labels + 1) * ($h + 2 * $obj->{'text_space'});
	$legend_w = ($max_len * $w) + 3 * $obj->{'text_space'} + 22;
	$obj->{'x_max'} -= $legend_w + 2 * $obj->{'text_space'} + 22;
	
	$obj->{'im'}->rectangle ($obj->{'x_max'} + 2 * $obj->{'text_space'},
				 $ymin,
				 $obj->{'x_max'} + 2 * $obj->{'text_space'} 
			             + $legend_w,
				 $ymin + $legend_h,
				 $black);
	
	$dash = $obj->{'dashed_lines'};
	$obj->{'dashed_lines'} = '';

	for (0..$#labels) {
	    $color = $obj->data_color($_);
	    
	    $obj->{'im'}->string (gdSmallFont,
				  $obj->{'x_max'} + 29,
				  $ymin + $obj->{'text_space'} 
				      + $_ * ($h + 2 * $obj->{'text_space'}),
				  $labels[$_],
				  $color);
	}

	$obj->{'dashed_lines'} = $dash;

	for (0..$#labels) {
	    $color = $obj->data_color($_);
	    
	    $obj->{'im'}->line ($obj->{'x_max'} + 7,
				$ymin + $obj->{'text_space'} + $h/2
				    + $_ * ($h + 2 * $obj->{'text_space'}),
				$obj->{'x_max'} + 27,
				$ymin + $obj->{'text_space'} + $h/2
				    + $_ * ($h + 2 * $obj->{'text_space'}),
				$color);
	}
    }
}

sub sort_data {
    my $obj = shift;
    my $dataref = $obj->{'data'};
    my ($order, $set, $type);
    my ($ref, $i, $j);

    if ($obj->{'nosort'}) { return }

    if (ref ($obj->{'sort'})) {
	($order, $set, $type) = @{$obj->{'sort'}};
    }
    else {
	$order = $obj->{'sort'};
    }

    $set = 0 unless ($set);
    $type = 'alpha' unless ($type);

    for $i (0..$#{$dataref->[0]}) {
	for $j (0..$#{$dataref}) {
	    $ref->[$i][$j] = $dataref->[$j][$i];
	}
    }
    
    if ($order eq 'asc') {
	if ($type eq 'alpha') {
            @{$ref} = sort {$Chart::Base::a->[$set] cmp $Chart::Base::b->[$set]}
	                       @{$ref};
	}
	else {
	    @{$ref} = sort {$Chart::Base::a->[$set] <=> $Chart::Base::b->[$set]}
	                       @{$ref};
	} 
    }
    else {
	if ($type eq 'alpha') {
            @{$ref} = sort {$Chart::Base::b->[$set] cmp $Chart::Base::a->[$set]}
	                       @{$ref};
        }
        else {
            @{$ref} = sort {$Chart::Base::b->[$set] <=> $Chart::Base::a->[$set]}
                               @{$ref};
        }   
    }
    
    for $i (0..$#{$dataref->[0]}) {
        for $j (0..$#{$dataref}) {
            $dataref->[$j][$i] = $ref->[$i][$j];
        }
    }
					    
    
    $obj->{'data'} = $dataref;
}

sub draw_axes {
    my $obj = shift;
    my $black = $obj->get_color ('black');
    
    $obj->{'im'}->rectangle ($obj->{'x_min'}, $obj->{'y_min'},
			     $obj->{'x_max'}, $obj->{'y_max'},
			     $black);
}

sub set_user_colors {
    my $obj = shift;
    my @rgbs = @{$obj->{'colors'}};

    for (@rgbs) {
	if ($_) {
	    $obj->{'im'}->colorAllocate (@{$_});
	}
    }
}

sub get_color {
    my $obj = shift;
    my $color = shift;
    my %colors = ('white' => [250,250,250],
		  'black' => [0,0,0],
		  'red' => [225,0,0],
		  'green' => [0,225,0],
		  'blue' => [0,0,225],
		  'purple' => [200,0,200],
		  'light_blue' => [0,200,200],
		  'yellow' => [225,225,0],
		  'orange' => [250,170,85],
		  'grey' => [200,200,200]);
    my @rgb = (defined($colors{$color})) ? @{$colors{$color}} : (0,0,0);

    return $obj->{'im'}->colorClosest(@rgb);
}

sub data_color {
    my $obj = shift;
    my $num = shift;
    my %colors = (0 => 'red',
		  1 => 'blue',
		  2 => 'green',
		  3 => 'purple',
		  4 => 'orange',
		  5 => 'light_blue',
		  6 => 'yellow');
    my ($col,%dots);

    $col = ($obj->{'colors'}->[$num]) 
    		? $obj->{'im'}->colorClosest (@{$obj->{'colors'}->[$num]})
		: $obj->get_color ($colors{$num});

    %dots = (4 => [$col],
             0 => [$col,$col,gdTransparent],
	     1 => [$col,$col,$col,$col,$col,$col,gdTransparent,gdTransparent,gdTransparent,$col,$col,$col,gdTransparent,gdTransparent,gdTransparent],
	     2 => [$col,$col,$col,$col,$col,$col,$col,$col,gdTransparent,gdTransparent,gdTransparent,gdTransparent],
	     3 => [$col,$col,$col,$col,gdTransparent,gdTransparent]);

    if ($obj->{'dashed_lines'} && $obj->{'dashed_lines'} ne '') {
        $obj->{'im'}->setStyle ((@{$dots{$num}}) 
	              ? @{$dots{$num}} : ($col,gdTransparent));
	return gdStyled;
    }
    else {
        return $col;
    }
}

1;