#===========================#
#                           #
#  Chart::Base              #
#  written by david bonner  #
#  dbonner@cs.bu.edu        #
#                           #
#===========================#


package Chart::Base;

use Carp;
use GD;
use strict;

$Chart::Base::VERSION = 0.92;

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

sub add_dataset {
    my $obj = shift;
    my $set;
    
    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 my $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' 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 (240, 0, 0);
    $self->{'im'}->colorAllocate (25, 140, 25);
    $self->{'im'}->colorAllocate (150, 150, 255);
    $self->{'im'}->colorAllocate (200,0,200);
    $self->{'im'}->colorAllocate (218,165,32);
}

sub copy_data {
    my $obj = shift;
    my $their_ref = shift;
    my $my_ref = [];

    if ($obj->{'data'}) {
	return -1;
    }
    else {
	for my $i (0..$#{$their_ref}) {
	    for my $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->{'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 (0..$#{$ref}-1) {
	       if ($#{$ref->[$_]} != $#{$ref->[$_+1]}) {
		   $mismatch = 1;
		   last CHECK;
	       }
    }

    if ($mismatch) {
	croak ("Data sets not of equal length");
    }
}
    
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) * $h;
	$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);
    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  #
    #===============#

    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'},
				 $obj->{'y_min'},
				 $obj->{'x_max'} + 2 * $obj->{'text_space'} 
			             + $legend_w,
				 $obj->{'y_min'} + $legend_h,
				 $black);
	
	for (0..$#labels) {
	    $color = $obj->data_color($_);
	    
	    $obj->{'im'}->string (gdSmallFont,
				  $obj->{'x_max'} + 7,
				  $obj->{'y_min'} + $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'},
				 $obj->{'y_min'},
				 $obj->{'x_max'} + 2 * $obj->{'text_space'} 
			             + $legend_w,
				 $obj->{'y_min'} + $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,
				  $obj->{'y_min'} + $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,
				$obj->{'y_min'} + $obj->{'text_space'} + $h/2
				    + $_ * ($h + 2 * $obj->{'text_space'}),
				$obj->{'x_max'} + 27,
				$obj->{'y_min'} + $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;

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

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

    $set = 1 unless ($set);
    $type = 'num' unless ($type);

    for my $i (0..$#{$dataref->[0]}) {
	for my $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 my $i (0..$#{$dataref->[0]}) {
        for my $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_max'},
			     $obj->{'x_max'}, $obj->{'y_min'},
			     $black);
}

sub get_color {
    my $obj = shift;
    my $color = shift;
    my %colors = ('white' => [250,250,250],
		  'black' => [0,0,0],
		  'red' => [240,0,0],
		  'green' => [25,140,25],
		  'blue' => [150,150,255],
		  'purple' => [200,0,200],
		  'goldenrod' => [218,165,32]);
    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 => 'goldenrod');
    my ($col,%dots);

    $col = $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}});
	return gdStyled;
    }
    else {
        return $col;
    }
}

1;

