#==================================================================== # Chart::Composite # # written by david bonner # dbonner@cs.bu.edu # # maintained by the Chart Group # Chart@wettzell.ifag.de # # #--------------------------------------------------------------------- # History: #---------- # $RCSfile: Composite.pm,v $ $Revision: 1.4 $ $Date: 2003/02/14 13:25:30 $ # $Author: dassing $ # $Log: Composite.pm,v $ # Revision 1.4 2003/02/14 13:25:30 dassing # Circumvent division of zeros # #==================================================================== package Chart::Composite; use Chart::Base '2.4.1'; use GD; use Carp; use strict; @Chart::Composite::ISA = qw(Chart::Base); $Chart::Composite::VERSION = '2.4.1'; #>>>>>>>>>>>>>>>>>>>>>>>>>># # public methods go here # #<<<<<<<<<<<<<<<<<<<<<<<<<<# ## have to override set, so we can pass the options to the ## sub-objects later sub set { my $self = shift; my %opts = @_; # basic error checking on the options, just warn 'em unless ($#_ % 2) { carp "Whoops, some option to be set didn't have a value.\n", "You might want to look at that.\n"; } # store the options they gave us unless ($self->{'opts'}) { $self->{'opts'} = {}; } # now set 'em for (keys %opts) { $self->{$_} = $opts{$_}; $self->{'opts'}{$_} = $opts{$_}; } # now return return; } ## get the information to turn the chart into an imagemap ## had to override it to reassemble the @data array correctly sub imagemap_dump { my $self = shift; my ($i, $j); my @map; my $dataset_count = 0; # croak if they didn't ask me to remember the data, or if they're asking # for the data before I generate it unless (($self->{'imagemap'} =~ /^true$/i) && $self->{'imagemap_data'}) { croak "You need to set the imagemap option to true, and then call the png method, before you can get the imagemap data"; } #make a copy of the imagemap data #this is the data of the first component for $i (1..$#{$self->{'sub_0'}->{'imagemap_data'}}) { for $j (0..$#{$self->{'sub_0'}->{'imagemap_data'}->[$i]}-1) { $map[$i][$j] = \@{$self->{'sub_0'}->{'imagemap_data'}->[$i][$j]} ; } $dataset_count++; } #and add the data of the second component for $i (1..$#{$self->{'sub_1'}->{'imagemap_data'}}) { for $j (0..$#{$self->{'sub_1'}->{'imagemap_data'}->[$i]}-1) { $map[$i+$dataset_count][$j] = \@{$self->{'sub_1'}->{'imagemap_data'}->[$i][$j]} ; } } # return their copy return \@map; } sub __print_array { my @a = @_; my $i; my $li = $#a; $li++; print STDERR "Anzahl der Elemente = $li\n"; $li--; for ($i=0; $i<=$li; $i++) { print STDERR "\t$i\t$a[$i]\n"; } } #>>>>>>>>>>>>>>>>>>>>>>>>>>># # private methods go here # #<<<<<<<<<<<<<<<<<<<<<<<<<<<# ## make sure the data isn't really weird ## and collect some basic info about it sub _check_data { my $self = shift; my $length = 0; # first things first, make sure we got the composite_info unless (($self->{'composite_info'}) && ($#{$self->{'composite_info'}} == 1)) { croak "Chart::Composite needs to be told what kind of components to use"; } # make sure we don't end up dividing by zero if they ask for # just one y_tick if ($self->{'y_ticks'} == 1) { $self->{'y_ticks'} = 2; carp "The number of y_ticks displayed must be at least 2"; } # remember the number of datasets $self->{'num_datasets'} = $#{$self->{'dataref'}}; # remember the number of points in the largest dataset $self->{'num_datapoints'} = 0; for (0..$self->{'num_datasets'}) { if (scalar(@{$self->{'dataref'}[$_]}) > $self->{'num_datapoints'}) { $self->{'num_datapoints'} = scalar(@{$self->{'dataref'}[$_]}); } } # find the longest x-tick label, and remember how long it is for (@{$self->{'dataref'}[0]}) { if (length ($_) > $length) { $length = length ($_); } } $self->{'x_tick_label_length'} = $length; # now split the data into sub-objects $self->_split_data; return; } ## create sub-objects for each type, store the appropriate ## data sets in each one, and stick the correct values into ## them (ie. 'gd_obj'); sub _split_data { my $self = shift; my @types = ($self->{'composite_info'}[0][0],$self->{'composite_info'}[1][0]); my ($ref, $i, $j); ## Already checked for number of components in _check_data, above. # # we can only do two at a time # if ($self->{'composite_info'}[2]) { # croak "Sorry, Chart::Composite can only do two chart types at a time"; # } # load the individual modules require "Chart/".$types[0].".pm"; require "Chart/".$types[1].".pm"; # create the sub-objects $self->{'sub_0'} = ("Chart::".$types[0])->new(); $self->{'sub_1'} = ("Chart::".$types[1])->new(); # set the options (set the min_val, max_val, brush_size, y_ticks, # # options intelligently so that the sub-objects don't get # confused) $self->{'sub_0'}->set (%{$self->{'opts'}}); $self->{'sub_1'}->set (%{$self->{'opts'}}); if (defined ($self->{'opts'}{'min_val1'})) { $self->{'sub_0'}->set ('min_val' => $self->{'opts'}{'min_val1'}); } if (defined ($self->{'opts'}{'max_val1'})) { $self->{'sub_0'}->set ('max_val' => $self->{'opts'}{'max_val1'}); } if (defined ($self->{'opts'}{'min_val2'})) { $self->{'sub_1'}->set ('min_val' => $self->{'opts'}{'min_val2'}); } if (defined ($self->{'opts'}{'max_val2'})) { $self->{'sub_1'}->set ('max_val' => $self->{'opts'}{'max_val2'}); } if ($self->{'opts'}{'y_ticks1'}) { $self->{'sub_0'}->set ('y_ticks' => $self->{'opts'}{'y_ticks1'}); } if ($self->{'opts'}{'y_ticks2'}) { $self->{'sub_1'}->set ('y_ticks' => $self->{'opts'}{'y_ticks2'}); } if ($self->{'opts'}{'brush_size1'}) { $self->{'sub_0'}->set ('brush_size' => $self->{'opts'}{'brush_size1'}); } if ($self->{'opts'}{'brush_size2'}) { $self->{'sub_1'}->set ('brush_size' => $self->{'opts'}{'brush_size2'}); } # f_y_tick for left and right axis if (defined ($self->{'opts'}{'f_y_tick1'})) { $self->{'sub_0'}->set ('f_y_tick' => $self->{'opts'}{'f_y_tick1'}); } if (defined ($self->{'opts'}{'f_y_tick2'})) { $self->{'sub_1'}->set ('f_y_tick' => $self->{'opts'}{'f_y_tick2'}); } # replace the gd_obj fields $self->{'sub_0'}->{'gd_obj'} = $self->{'gd_obj'}; $self->{'sub_1'}->{'gd_obj'} = $self->{'gd_obj'}; # let the sub-objects know they're sub-objects $self->{'sub_0'}->{'component'} = 'true'; $self->{'sub_1'}->{'component'} = 'true'; # give each sub-object its data $self->{'component_datasets'} = []; for $i (0..1) { $ref = []; $self->{'component_datasets'}[$i] = $self->{'composite_info'}[$i][1]; push @{$ref}, $self->{'dataref'}[0]; for $j (@{$self->{'composite_info'}[$i][1]}) { $self->_color_role_to_index('dataset'.($j-1)); # allocate color index push @{$ref}, $self->{'dataref'}[$j]; } $self->{'sub_'.$i}->_copy_data ($ref); } # and let them check it $self->{'sub_0'}->_check_data; $self->{'sub_1'}->_check_data; # realign the y-axes if they want if ($self->{'same_y_axes'} =~ /^true$/i) { if ($self->{'sub_0'}{'min_val'} < $self->{'sub_1'}{'min_val'}) { $self->{'sub_1'}{'min_val'} = $self->{'sub_0'}{'min_val'}; } else { $self->{'sub_0'}{'min_val'} = $self->{'sub_1'}{'min_val'}; } if ($self->{'sub_0'}{'max_val'} > $self->{'sub_1'}{'max_val'}) { $self->{'sub_1'}{'max_val'} = $self->{'sub_0'}{'max_val'}; } else { $self->{'sub_0'}{'max_val'} = $self->{'sub_1'}{'max_val'}; } $self->{'sub_0'}->_check_data; $self->{'sub_1'}->_check_data; } # find out how big the y-tick labels will be from sub_0 and sub_1 $self->{'y_tick_label_length1'} = $self->{'sub_0'}->{'y_tick_label_length'}; $self->{'y_tick_label_length2'} = $self->{'sub_1'}->{'y_tick_label_length'}; # now return return; } sub _draw_legend { my $self = shift; my ($length); # check to see if they have as many labels as datasets, # warn them if not if (($#{$self->{'legend_labels'}} >= 0) && ((scalar(@{$self->{'legend_labels'}})) != $self->{'num_datasets'})) { carp "The number of legend labels and datasets doesn\'t match"; } # init a field to store the length of the longest legend label unless ($self->{'max_legend_label'}) { $self->{'max_legend_label'} = 0; } # fill in the legend labels, find the longest one for (1..$self->{'num_datasets'}) { unless ($self->{'legend_labels'}[$_-1]) { $self->{'legend_labels'}[$_-1] = "Dataset $_"; } $length = length($self->{'legend_labels'}[$_-1]); if ($length > $self->{'max_legend_label'}) { $self->{'max_legend_label'} = $length; } } # different legend types if ($self->{'legend'} eq 'bottom') { $self->_draw_bottom_legend; } elsif ($self->{'legend'} eq 'right') { $self->_draw_right_legend; } elsif ($self->{'legend'} eq 'left') { $self->_draw_left_legend; } elsif ($self->{'legend'} eq 'top') { $self->_draw_top_legend; } elsif ($self->{'legend'} eq 'none') { $self->_draw_none_legend; } else { carp "I can't put a legend there\n"; } # and return return 1; } ## put the legend on the top of the data plot sub _draw_top_legend { my $self = shift; my @labels = @{$self->{'legend_labels'}}; my ($x1, $y1, $x2, $y2, $empty_width, $max_label_width, $cols, $rows, $color); my ($col_width, $row_height, $i, $j, $r, $c, $index, $x, $y, $sub, $w, $h); my ($yh,$yi); # for boxing legends my $font = $self->{'legend_font'}; my (%colors, @datasets); my $max_legend_example=0; $yh=0; # copy the current boundaries into the sub-objects $self->_sub_update; # init the legend_example_height $self->_legend_example_height_init; ## Make datasetI numbers match indexes of @{ $self->{'dataref'} }[1.....]. # # modify the dataset color table entries to avoid duplicating # # dataset colors (this limits the number of possible data sets # # for each component to 8) # for (0..7) { # $self->{'sub_1'}{'color_table'}{'dataset'.$_} # = $self->{'color_table'}{'dataset'.($_+8)}; # } # modify the dataset color table entries to avoid duplicating # dataset colors. my ($n0, $n1) = map { scalar @{ $self->{'composite_info'}[$_][1] } } 0..1; for (0..$n1-1) { $self->{'sub_1'}{'color_table'}{'dataset'.$_} = $self->{'color_table'}{'dataset'.($_+$n0)}; } # make sure we use the right colors for the legend @datasets = @{$self->{'composite_info'}[0][1]}; $i = 0; for (0..$#datasets) { $colors{$datasets[$_]-1} = $self->{'color_table'}{'dataset'.($i)}; $i++; } @datasets = @{$self->{'composite_info'}[1][1]}; $i = 0; for (0..$#datasets) { $colors{$datasets[$_]-1} = $self->{'color_table'}{'dataset'.($i+$n0)}; $i++; } # make sure we're using a real font unless ((ref ($font)) eq 'GD::Font') { croak "The subtitle font you specified isn\'t a GD Font object"; } # get the size of the font ($h, $w) = ($font->height, $font->width); # get some base x coordinates $x1 = $self->{'curr_x_min'} + $self->{'graph_border'} + $self->{'y_tick_label_length1'} * $self->{'tick_label_font'}->width + $self->{'tick_len'} + (3 * $self->{'text_space'}); $x2 = $self->{'curr_x_max'} - $self->{'graph_border'} - $self->{'y_tick_label_length2'} * $self->{'tick_label_font'}->width - $self->{'tick_len'} - (3 * $self->{'text_space'}); if ($self->{'y_label'}) { $x1 += $self->{'label_font'}->height + 2 * $self->{'text_space'}; } if ($self->{'y_label2'}) { $x2 -= $self->{'label_font'}->height + 2 * $self->{'text_space'}; } # figure out how wide the widest label is, then figure out how many # columns we can fit into the allotted space $empty_width = $x2 - $x1 - (2 * $self->{'legend_space'}); $max_label_width = $self->{'max_legend_label'} * $self->{'legend_font'}->width + 4 * $self->{'text_space'} + $self->{'legend_example_size'}; $cols = int ($empty_width / $max_label_width); unless ($cols) { $cols = 1; } $col_width = $empty_width / $cols; # figure out how many rows we need and how tall they are $rows = int ($self->{'num_datasets'} / $cols); unless (($self->{'num_datasets'} % $cols) == 0) { $rows++; } unless ($rows) { $rows = 1; } $row_height = $h + $self->{'text_space'}; # box the legend off $y1 = $self->{'curr_y_min'}; $y2 = $self->{'curr_y_min'} + $self->{'text_space'} + ($rows * $row_height) + (2 * $self->{'legend_space'}); $self->{'gd_obj'}->rectangle($x1, $y1, $x2, $y2, $self->_color_role_to_index('misc')); $max_legend_example = $y2-$y1; # leave some space inside the legend $x1 += $self->{'legend_space'} + $self->{'text_space'}; $x2 -= $self->{'legend_space'}; $y1 += $self->{'legend_space'} + $self->{'text_space'}; $y2 -= $self->{'legend_space'} + $self->{'text_space'}; # draw in the actual legend $r = 0; # current row $c = 0; # current column $yi= 0; # current dataset for $i (0..1) { for $j (0..$#{$self->{'component_datasets'}[$i]}) { # get the color $color = $self->{'sub_'.$i}->{'color_table'}{'dataset'.$j}; $index = $self->{'component_datasets'}[$i][$j] - 1; # index in label list # find the x-y coordinates for the beginning of the example line $x = $x1 + ($col_width * $c); $y = $y1 + ($row_height * $r) + $h/2; # draw the example line if legend_example_height==1 or ==0 if ($rows==1) { if ($self->{'legend_example_height'.$yi}<$max_legend_example) { $yh = $self->{'legend_example_height'.$yi}; } else { $yh = $max_legend_example; } } else { if ($self->{'legend_example_height'.$yi}<$row_height) { $yh = $self->{'legend_example_height'.$yi}; } else { $yh = $row_height; } } $yi++; if ($yh <= 1) { $self->{'gd_obj'}->line ($x, $y, $x + $self->{'legend_example_size'}, $y, $color); } else { # draw the example bar if legend_example_height > 1 $yh = int($yh / 2); $self->{'gd_obj'}->filledRectangle ($x, $y-$yh, $x + $self->{'legend_example_size'}, $y+$yh, $color); } # find the x-y coordinates for the beginning of the label $x += $self->{'legend_example_size'} + 2 * $self->{'text_space'}; $y -= $h/2; # now draw the label $self->{'gd_obj'}->string($font, $x, $y, $labels[$index], $color); # keep track of which row/column we're using $r = ($r + 1) % $rows; if ($r == 0) { $c++; } } } # mark of the space used $self->{'curr_y_min'} += ($rows * $row_height) + $self->{'text_space'} + 2 * $self->{'legend_space'}; return; } ## put the legend on the right of the chart sub _draw_right_legend { my $self = shift; my @labels = @{$self->{'legend_labels'}}; my ($x1, $x2, $x3, $y1, $y2, $width, $color, $misccolor, $w, $h); my ($yh)=0; # for boxing legend my $font = $self->{'legend_font'}; my (%colors, @datasets, $i); my $max_legend_example = 0; # copy the current boundaries and colors into the sub-objects $self->_sub_update; # init the legend exapmle height $self->_legend_example_height_init; # # modify the dataset color table entries to avoid duplicating # # dataset colors (this limits the number of possible data sets # # for each component to 8) # for (0..7) { # $self->{'sub_1'}{'color_table'}{'dataset'.$_} # = $self->{'color_table'}{'dataset'.($_+8)}; # } # modify the dataset color table entries to avoid duplicating # dataset colors. my ($n0, $n1) = map { scalar @{ $self->{'composite_info'}[$_][1] } } 0..1; for (0..$n1-1) { $self->{'sub_1'}{'color_table'}{'dataset'.$_} = $self->{'color_table'}{'dataset'.($_+$n0)}; } # make sure we use the right colors for the legend @datasets = @{$self->{'composite_info'}[0][1]}; $i = 0; for (0..$#datasets) { $colors{$datasets[$_]-1} = $self->{'color_table'}{'dataset'.($_)}; $i++; } @datasets = @{$self->{'composite_info'}[1][1]}; $i = 0; for (0..$#datasets) { $colors{$datasets[$_]-1} = $self->{'color_table'}{'dataset'.($i+$n0)}; $i++; } # make sure we're using a real font unless ((ref ($font)) eq 'GD::Font') { croak "The subtitle font you specified isn\'t a GD Font object"; } # get the size of the font ($h, $w) = ($font->height, $font->width); # get the miscellaneous color $misccolor = $self->_color_role_to_index('misc'); # find out how wide the largest label is $width = (2 * $self->{'text_space'}) + ($self->{'max_legend_label'} * $w) + $self->{'legend_example_size'} + (2 * $self->{'legend_space'}); # box the thing off $x1 = $self->{'curr_x_max'} - $width; $x2 = $self->{'curr_x_max'}; $y1 = $self->{'curr_y_min'} + $self->{'graph_border'} ; $y2 = $self->{'curr_y_min'} + $self->{'graph_border'} + $self->{'text_space'} + ($self->{'num_datasets'} * ($h + $self->{'text_space'})) + (2 * $self->{'legend_space'}); $self->{'gd_obj'}->rectangle ($x1, $y1, $x2, $y2, $misccolor); # leave that nice space inside the legend box $x1 += $self->{'legend_space'}; $y1 += $self->{'legend_space'} + $self->{'text_space'}; # now draw the actual legend for (0..$#labels) { # get the color $color = $colors{$_}; # find the max_legend_example $max_legend_example = $self->{'legend_space'}+$h; # find the x-y coords $x2 = $x1; $x3 = $x2 + $self->{'legend_example_size'}; $y2 = $y1 + ($_ * ($self->{'text_space'} + $h)) + $h/2; # draw the example line if legend_example_height==1 or ==0 if ($self->{'legend_example_height'.$_}<$max_legend_example) { $yh = $self->{'legend_example_height'.$_}; } else { $yh = $max_legend_example; } if ($yh <= 1) { $self->{'gd_obj'}->line ($x2, $y2, $x3, $y2, $color); } else { $yh = int($yh / 2); $self->{'gd_obj'}->filledRectangle ($x2, $y2-$yh, $x3, $y2+$yh, $color); } # now the label $x2 = $x3 + (2 * $self->{'text_space'}); $y2 -= $h/2; $self->{'gd_obj'}->string ($font, $x2, $y2, $labels[$_], $color); } # mark off the used space $self->{'curr_x_max'} -= $width; # and return return; } ## draw the legend at the left of the data plot sub _draw_left_legend { my $self = shift; my @labels = @{$self->{'legend_labels'}}; my ($x1, $x2, $x3, $y1, $y2, $width, $color, $misccolor, $w, $h); my $yh; # for boxing legend my $font = $self->{'legend_font'}; my (%colors, @datasets, $i); my $max_legend_example=0; # copy the current boundaries and colors into the sub-objects $self->_sub_update; # init the legend_example height $self->_legend_example_height_init; # # modify the dataset color table entries to avoid duplicating # # dataset colors (this limits the number of possible data sets # # for each component to 8) # for (0..7) { # $self->{'sub_1'}{'color_table'}{'dataset'.$_} # = $self->{'color_table'}{'dataset'.($_+8)}; # } # modify the dataset color table entries to avoid duplicating # dataset colors. my ($n0, $n1) = map { scalar @{ $self->{'composite_info'}[$_][1] } } 0..1; for (0..$n1-1) { $self->{'sub_1'}{'color_table'}{'dataset'.$_} = $self->{'color_table'}{'dataset'.($_+$n0)}; } # make sure we use the right colors for the legend @datasets = @{$self->{'composite_info'}[0][1]}; $i = 0; for (0..$#datasets) { $colors{$datasets[$_]-1} = $self->{'color_table'}{'dataset'.($i)}; $i++; } @datasets = @{$self->{'composite_info'}[1][1]}; $i = 0; for (0..$#datasets) { $colors{$datasets[$_]-1} = $self->{'color_table'}{'dataset'.($i+$n0)}; $i++; } # make sure we're using a real font unless ((ref ($font)) eq 'GD::Font') { croak "The subtitle font you specified isn\'t a GD Font object"; } # get the size of the font ($h, $w) = ($font->height, $font->width); # get the miscellaneous color $misccolor = $self->_color_role_to_index('misc'); # find out how wide the largest label is $width = (2 * $self->{'text_space'}) + ($self->{'max_legend_label'} * $w) + $self->{'legend_example_size'} + (2 * $self->{'legend_space'}); # get some base x-y coordinates $x1 = $self->{'curr_x_min'}; $x2 = $self->{'curr_x_min'} + $width; $y1 = $self->{'curr_y_min'} + $self->{'graph_border'} ; $y2 = $self->{'curr_y_min'} + $self->{'graph_border'} + $self->{'text_space' } + ($self->{'num_datasets'} * ($h + $self->{'text_space'})) + (2 * $self->{'legend_space'}); # box the legend off $self->{'gd_obj'}->rectangle ($x1, $y1, $x2, $y2, $misccolor); # leave that nice space inside the legend box $x1 += $self->{'legend_space'}; $y1 += $self->{'legend_space'} + $self->{'text_space'}; # now draw the actual legend for (0..$#labels) { # get the color $color = $colors{$_}; # find the max_legend_example $max_legend_example = $self->{'legend_space'} + $h; # find the x-y coords $x2 = $x1; $x3 = $x2 + $self->{'legend_example_size'}; $y2 = $y1 + ($_ * ($self->{'text_space'} + $h)) + $h/2; # draw the example line if legend_example_height==1 or ==0 if ($self->{'legend_example_height'.$_}<$max_legend_example) { $yh = $self->{'legend_example_height'.$_}; } else { $yh = $max_legend_example; } if ($yh <= 1) { $self->{'gd_obj'}->line ($x2, $y2, $x3, $y2, $color); } else { # draw the example bar if legend_example_height > 1 $yh = int($yh / 2); $self->{'gd_obj'}->filledRectangle ($x2, $y2-$yh, $x3, $y2+$yh, $color); } # now the label $x2 = $x3 + (2 * $self->{'text_space'}); $y2 -= $h/2; $self->{'gd_obj'}->string ($font, $x2, $y2, $labels[$_], $color); } # mark off the used space $self->{'curr_x_min'} += $width; # and return return 1; } ## draw the legend on the bottom of the data plot sub _draw_bottom_legend { my $self = shift; my @labels = @{$self->{'legend_labels'}}; my ($x1, $y1, $x2, $y2, $empty_width, $max_label_width, $cols, $rows, $color); my ($col_width, $row_height, $i, $j, $r, $c, $index, $x, $y, $sub, $w, $h); my ($yh,$yi); # for boxing legend my $font = $self->{'legend_font'}; my (%colors, @datasets); my $max_legend_example=0; $yh=0; # copy the current boundaries and colors into the sub-objects $self->_sub_update; # init the legend example height $self->_legend_example_height_init; # # modify the dataset color table entries to avoid duplicating # # dataset colors (this limits the number of possible data sets # # for each component to 8) # for (0..7) { # $self->{'sub_1'}{'color_table'}{'dataset'.$_} # = $self->{'color_table'}{'dataset'.($_+8)}; # } # modify the dataset color table entries to avoid duplicating # dataset colors. my ($n0, $n1) = map { scalar @{ $self->{'composite_info'}[$_][1] } } 0..1; for (0..$n1-1) { $self->{'sub_1'}{'color_table'}{'dataset'.$_} = $self->{'color_table'}{'dataset'.($_+$n0)}; } @datasets = @{$self->{'composite_info'}[0][1]}; $i = 0; for (0..$#datasets) { $colors{$datasets[$_]-1} = $self->{'color_table'}{'dataset'.($i)}; $i++; } @datasets = @{$self->{'composite_info'}[1][1]}; $i = 0; for (0..$#datasets) { $colors{$datasets[$_]-1} = $self->{'color_table'}{'dataset'.($i+$n0)}; $i++; } # make sure we're using a real font unless ((ref ($font)) eq 'GD::Font') { croak "The subtitle font you specified isn\'t a GD Font object"; } # get the size of the font ($h, $w) = ($font->height, $font->width); # figure out how many columns we can fit $x1 = $self->{'curr_x_min'} + $self->{'graph_border'} + $self->{'y_tick_label_length1'} * $self->{'tick_label_font'}->width + $self->{'tick_len'} + (3 * $self->{'text_space'}); $x2 = $self->{'curr_x_max'} - $self->{'graph_border'} - $self->{'y_tick_label_length2'} * $self->{'tick_label_font'}->width - $self->{'tick_len'} - (3 * $self->{'text_space'}); if ($self->{'y_label'}) { $x1 += $self->{'label_font'}->height + 2 * $self->{'text_space'}; } if ($self->{'y_label2'}) { $x2 -= $self->{'label_font'}->height + 2 * $self->{'text_space'}; } $empty_width = $x2 - $x1 - (2 * $self->{'legend_space'}); $max_label_width = $self->{'max_legend_label'} * $self->{'legend_font'}->width + 4 * $self->{'text_space'} + $self->{'legend_example_size'}; $cols = int ($empty_width / $max_label_width); unless ($cols) { $cols = 1; } $col_width = $empty_width / $cols; # figure out how many rows we need $rows = int ($self->{'num_datasets'} / $cols); unless (($self->{'num_datasets'} % $cols) == 0) { $rows++; } unless ($rows) { $rows = 1; } $row_height = $h + $self->{'text_space'}; # box it off $y1 = $self->{'curr_y_max'} - $self->{'text_space'} - ($rows * $row_height) - (2 * $self->{'legend_space'}); $y2 = $self->{'curr_y_max'}; $self->{'gd_obj'}->rectangle($x1, $y1, $x2, $y2, $self->_color_role_to_index('misc')); # get the max_legend_example_height $max_legend_example = $y2-$y1; $x1 += $self->{'legend_space'} + $self->{'text_space'}; $x2 -= $self->{'legend_space'}; $y1 += $self->{'legend_space'} + $self->{'text_space'}; $y2 -= $self->{'legend_space'} + $self->{'text_space'}; # draw in the actual legend $r = 0; $c = 0; $yi= 0; # current dataset for $i (0..1) { for $j (0..$#{$self->{'component_datasets'}[$i]}) { $color = $self->{'sub_'.$i}->{'color_table'}{'dataset'.$j}; $index = $self->{'component_datasets'}[$i][$j] - 1; $x = $x1 + ($col_width * $c); $y = $y1 + ($row_height * $r) + $h/2; # draw the example line if legend_example_height==1 or ==0 if ($rows == 1) { if ($self->{'legend_example_height'.$yi} < $max_legend_example) { $yh = $self->{'legend_example_height'.$yi}; } else { $yh = $max_legend_example; } } else { if ($self->{'legend_example_height'.$yi} < $row_height) { $yh = $self->{'legend_example_height'.$yi}; } else { $yh = $row_height; } } $yi++; if ($yh <= 1) { $self->{'gd_obj'}->line ($x, $y, $x + $self->{'legend_example_size'}, $y, $color); } else { # draw the example bar if legend_example_height > 1 $yh = int($yh / 2); $self->{'gd_obj'}->filledRectangle ($x, $y-$yh, $x + $self->{'legend_example_size'}, $y+$yh, $color); } $x += $self->{'legend_example_size'} + 2 * $self->{'text_space'}; $y -= $h/2; $self->{'gd_obj'}->string($font, $x, $y, $labels[$index], $color); # keep track of which row/column we're using $r = ($r + 1) % $rows; if ($r == 0) { $c++; } } } # mark of the space used $self->{'curr_y_max'} -= ($rows * $row_height) + $self->{'text_space'} + 2 * $self->{'legend_space'}; return; } # no legend to draw.. just update the color tables for subs sub _draw_none_legend { my $self = shift; $self->_sub_update(); # for (0..7) { # $self->{'sub_1'}{'color_table'}{'dataset'.$_} # = $self->{'color_table'}{'dataset'.($_+8)}; # } # modify the dataset color table entries to avoid duplicating # dataset colors. my ($n0, $n1) = map { scalar @{ $self->{'composite_info'}[$_][1] } } 0..1; for (0..$n1-1) { $self->{'sub_1'}{'color_table'}{'dataset'.$_} = $self->{'color_table'}{'dataset'.($_+$n0)}; } } ## draw the ticks and tick labels sub _draw_ticks { my $self = shift; # draw the x ticks $self->_draw_x_ticks; # update the boundaries in the sub-objects $self->_boundary_update ($self, $self->{'sub_0'}); $self->_boundary_update ($self, $self->{'sub_1'}); # now the y ticks $self->_draw_y_ticks; # then return return; } ## draw the 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); my ($x1, $x2, $y1, $y2); my ($width, $delta); my ($stag); $self->{'grid_data'}->{'x'} = []; # make sure we got a real font unless ((ref $font) eq 'GD::Font') { croak "The tick label font you specified isn\'t a GD Font object"; } # get the height and width of the font ($h, $w) = ($font->height, $font->width); # allow for the amount of space the y-ticks will push the # axes over to the right and to the left ## _draw_y_ticks allows 3 * text_space, not 2 * ; this caused mismatch between ## the ticks (and grid lines) and the data. # $x1 = $self->{'curr_x_min'} + ($w * $self->{'y_tick_label_length1'}) # + (2 * $self->{'text_space'}) + $self->{'tick_len'}; # $x2 = $self->{'curr_x_max'} - ($w * $self->{'y_tick_label_length2'}) # - (2 * $self->{'text_space'}) - $self->{'tick_len'}; $x1 = $self->{'curr_x_min'} + ($w * $self->{'y_tick_label_length1'}) + (3 * $self->{'text_space'}) + $self->{'tick_len'}; $x2 = $self->{'curr_x_max'} - ($w * $self->{'y_tick_label_length2'}) - (3 * $self->{'text_space'}) - $self->{'tick_len'}; $y1 = $self->{'curr_y_max'} - $h - $self->{'text_space'}; # get the delta value, figure out how to draw the labels $width = $x2 - $x1; $delta = $width / ( $self->{'num_datapoints'} > 0 ? $self->{'num_datapoints'} : 1 ); if ($delta <= ($self->{'x_tick_label_length'} * $w)) { unless ($self->{'x_ticks'} =~ /^vertical$/i) { $self->{'x_ticks'} = 'staggered'; } } # now draw the labels if ($self->{'x_ticks'} =~ /^normal$/i) { # normal ticks if ($self->{'skip_x_ticks'}) { for (0..int(($self->{'num_datapoints'}-1)/$self->{'skip_x_ticks'})) { $x2 = $x1 + ($delta/2) + ($delta*($_*$self->{'skip_x_ticks'})) - ($w*length( $self->{'f_x_tick'}->($data->[0][$_* $self->{'skip_x_ticks'}]))) / 2; $self->{'gd_obj'}->string($font, $x2, $y1, $self->{'f_x_tick'}->($data->[0][$_*$self->{'skip_x_ticks'}]), $textcolor); } } elsif ($self->{'custom_x_ticks'}) { for (@{$self->{'custom_x_ticks'}}) { $x2 = $x1 + ($delta/2) + ($delta*$_) - ($w*length( $self->{'f_x_tick'}->($data->[0][$_]))) / 2; $self->{'gd_obj'}->string($font, $x2, $y1, $self->{'f_x_tick'}->($data->[0][$_]), $textcolor); } } else { for (0..$self->{'num_datapoints'}-1) { $x2 = $x1 + ($delta/2) + ($delta*$_) - ($w*length($self->{'f_x_tick'}->($data->[0][$_]))) / 2; $self->{'gd_obj'}->string($font, $x2, $y1, $self->{'f_x_tick'}->($data->[0][$_]), $textcolor); } } } elsif ($self->{'x_ticks'} =~ /^staggered$/i) { # staggered ticks if ($self->{'skip_x_ticks'}) { $stag = 0; for (0..int(($self->{'num_datapoints'}-1)/$self->{'skip_x_ticks'})) { $x2 = $x1 + ($delta/2) + ($delta*($_*$self->{'skip_x_ticks'})) - ($w*length( $self->{'f_x_tick'}->($data->[0][$_*$self->{'skip_x_ticks'}]))) / 2; if (($stag % 2) == 1) { $y1 -= $self->{'text_space'} + $h; } $self->{'gd_obj'}->string($font, $x2, $y1, $self->{'f_x_tick'}->($data->[0][$_*$self->{'skip_x_ticks'}]), $textcolor); if (($stag % 2) == 1) { $y1 += $self->{'text_space'} + $h; } $stag++; } } elsif ($self->{'custom_x_ticks'}) { $stag = 0; for (sort (@{$self->{'custom_x_ticks'}})) { $x2 = $x1 + ($delta/2) + ($delta*$_) - ($w*length( $self->{'f_x_tick'}->($data->[0][$_]))) / 2; if (($stag % 2) == 1) { $y1 -= $self->{'text_space'} + $h; } $self->{'gd_obj'}->string($font, $x2, $y1, $self->{'f_x_tick'}->($data->[0][$_]), $textcolor); if (($stag % 2) == 1) { $y1 += $self->{'text_space'} + $h; } $stag++; } } else { for (0..$self->{'num_datapoints'}-1) { $x2 = $x1 + ($delta/2) + ($delta*$_) - ($w*length( $self->{'f_x_tick'}->($data->[0][$_]))) / 2; if (($_ % 2) == 1) { $y1 -= $self->{'text_space'} + $h; } $self->{'gd_obj'}->string($font, $x2, $y1, $self->{'f_x_tick'}->($data->[0][$_]), $textcolor); if (($_ % 2) == 1) { $y1 += $self->{'text_space'} + $h; } } } } elsif ($self->{'x_ticks'} =~ /^vertical$/i) { # vertical ticks $y1 = $self->{'curr_y_max'} - $self->{'text_space'}; if ( defined($self->{'skip_x_ticks'}) && $self->{'skip_x_ticks'} > 1) { for (0..int(($self->{'num_datapoints'}-1)/$self->{'skip_x_ticks'})) { $x2 = $x1 + ($delta/2) + ($delta*($_*$self->{'skip_x_ticks'})) - $h/2; $y2 = $y1 - (($self->{'x_tick_label_length'} - length( $self->{'f_x_tick'}->($data->[0][$_*$self->{'skip_x_ticks'}]))) * $w); $self->{'gd_obj'}->stringUp($font, $x2, $y2, $self->{'f_x_tick'}->($data->[0][$_*$self->{'skip_x_ticks'}]), $textcolor); } } elsif ($self->{'custom_x_ticks'}) { for (@{$self->{'custom_x_ticks'}}) { $x2 = $x1 + ($delta/2) + ($delta*$_) - $h/2; $y2 = $y1 - (($self->{'x_tick_label_length'} - length( $self->{'f_x_tick'}->($data->[0][$_]))) * $w); $self->{'gd_obj'}->stringUp($font, $x2, $y2, $self->{'f_x_tick'}->($data->[0][$_]), $textcolor); } } else { for (0..$self->{'num_datapoints'}-1) { $x2 = $x1 + ($delta/2) + ($delta*$_) - $h/2; $y2 = $y1 - (($self->{'x_tick_label_length'} - length( $self->{'f_x_tick'}->($data->[0][$_]))) * $w); $self->{'gd_obj'}->stringUp($font, $x2, $y2, $self->{'f_x_tick'}->($data->[0][$_]), $textcolor); } } } else { # error time carp "I don't understand the type of x-ticks you specified"; } # update the current y-max value if ($self->{'x_ticks'} =~ /^normal$/i) { $self->{'curr_y_max'} -= $h + (2 * $self->{'text_space'}); } elsif ($self->{'x_ticks'} =~ /^staggered$/i) { $self->{'curr_y_max'} -= (2 * $h) + (3 * $self->{'text_space'}); } elsif ($self->{'x_ticks'} =~ /^vertical$/i) { $self->{'curr_y_max'} -= ($w * $self->{'x_tick_label_length'}) + (2 * $self->{'text_space'}); } # now plot the ticks $y1 = $self->{'curr_y_max'}; $y2 = $self->{'curr_y_max'} - $self->{'tick_len'}; if ($self->{'skip_x_ticks'}) { for (0..int(($self->{'num_datapoints'}-1)/$self->{'skip_x_ticks'})) { $x2 = $x1 + ($delta/2) + ($delta*($_*$self->{'skip_x_ticks'})); $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; } } } elsif ($self->{'custom_x_ticks'}) { for (@{$self->{'custom_x_ticks'}}) { $x2 = $x1 + ($delta/2) + ($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; } } } else { for (0..$self->{'num_datapoints'}-1) { $x2 = $x1 + ($delta/2) + ($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; } } } # update the current y-max value $self->{'curr_y_max'} -= $self->{'tick_len'}; # and return return; } ## draw the y-ticks and their labels sub _draw_y_ticks { my $self = shift; # let the first guy do his $self->{'sub_0'}->_draw_y_ticks ('left'); # and update the other two objects $self->_boundary_update ($self->{'sub_0'}, $self); $self->_boundary_update ($self->{'sub_0'}, $self->{'sub_1'}); # now draw the other ones $self->{'sub_1'}->_draw_y_ticks ('right'); # and update the other two objects $self->_boundary_update ($self->{'sub_1'}, $self); $self->_boundary_update ($self->{'sub_1'}, $self->{'sub_0'}); # then return return; } ## finally get around to plotting the data sub _draw_data { my $self = shift; # do a grey background if they want it if ($self->{'grey_background'} =~ /^true$/i) { $self->_grey_background; $self->{'sub_0'}->{'grey_background'} = 'false'; $self->{'sub_1'}->{'grey_background'} = 'false'; } # draw grid again if necessary (if grey background ruined it..) unless ($self->{grey_background} !~ /^true$/i) { $self->_draw_grid_lines if ($self->{grid_lines} =~ /^true$/i); $self->_draw_x_grid_lines if ($self->{x_grid_lines} =~ /^true$/i); $self->_draw_y_grid_lines if ($self->{y_grid_lines} =~ /^true$/i); $self->_draw_y2_grid_lines if ($self->{y2_grid_lines} =~ /^true$/i); } # do a final bounds update $self->_boundary_update ($self, $self->{'sub_0'}); $self->_boundary_update ($self, $self->{'sub_1'}); # init the imagemap data field if they wanted it if ($self->{'imagemap'} =~ /^true$/i) { $self->{'imagemap_data'} = []; } # now let the component modules go to work $self->{'sub_0'}->_draw_data; $self->{'sub_1'}->_draw_data; return; } ## update all the necessary information in the sub-objects sub _sub_update { my $self = shift; my $sub0 = $self->{'sub_0'}; my $sub1 = $self->{'sub_1'}; # update the boundaries $self->_boundary_update ($self, $sub0); $self->_boundary_update ($self, $sub1); # copy the color tables $sub0->{'color_table'} = { %{$self->{'color_table'}} }; $sub1->{'color_table'} = { %{$self->{'color_table'}} }; # now return return; } ## copy the current gd_obj boundaries from one object to another sub _boundary_update { my $self = shift; my $from = shift; my $to = shift; $to->{'curr_x_min'} = $from->{'curr_x_min'}; $to->{'curr_x_max'} = $from->{'curr_x_max'}; $to->{'curr_y_min'} = $from->{'curr_y_min'}; $to->{'curr_y_max'} = $from->{'curr_y_max'}; return; } sub _draw_y_grid_lines { my ($self) = shift; $self->{'sub_0'}->_draw_y_grid_lines(); return; } sub _draw_y2_grid_lines { my ($self) = shift; $self->{'sub_1'}->_draw_y2_grid_lines(); return; } # init the legend_example_height_values sub _legend_example_height_init { my $self = shift; my $a = $self->{'num_datasets'}; my ($b, $e) =(0,0); my $bis='..'; if ($self->{'legend_example_height'} =~ /^false$/i ) { for my $i (0..$a) { $self->{'legend_example_height'.$i} = 1; } } if ($self->{'legend_example_height'} =~ /^true$/i ) { for my $i (0..$a) { if (defined($self->{'legend_example_height'.$i})) { } else { ($self->{'legend_example_height'.$i}) = 1;} } for $b (0..$a) { for $e (0..$a) { my $anh = sprintf($b.$bis.$e); if (defined($self->{'legend_example_height'.$anh})) { if ($b>$e) {croak "Please reverse the datasetnumber in legend_example_height\n";} for (my $n=$b;$n<=$e;$n++) { $self->{'legend_example_height'.$n} = $self->{'legend_example_height'.$anh}; } } } } } } ## be a good module and return 1 1;