File Coverage

blib/lib/Chart/Plot.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #=====================================================================#
2             # Chart::Plot -- Front end to GD.pm for plotting two dimensional data #
3             # by Sanford Morton #
4             #=====================================================================#
5              
6             # Changes:
7             # v 0.0 - 08 March 1998
8             # first version
9             # v 0.01 - 09 March 1998;
10             # - _getOM reports >= max instead of >;
11             # - fixed bug in setData() data check
12             # v 0.02 - 10 March 1998;
13             # - changed error handling in setData() (public method) which
14             # now returns undef on success and sets $self->error ;
15             # - changed legend to title (public method)
16             # - adjusted horizontal tick labels up a bit
17             # v 0.03 - 15 March 1998
18             # - added colors and dashed line options to dataset graph style
19             # - added option to pass dataset as two arrays (@xdata, @ydata)
20             # - added hack for case om == max
21             # v 0.04 - 15 March 1998
22             # - added general purpose setGraphOptions()
23             # v 0.05 - 18 March 1998
24             # _ added synopsis to pod
25             # - added getBounds()
26             # - Hor axis label is set below and right centered or justified.
27             # - additional vertical offset if title is present; larger font
28             # v 0.06 - 22 March 1998
29             # - removed title, offset and axis label methods in favor of
30             # setGraphOptions()
31             # - added getBounds()
32             # v 0.07 - 29 May 1998
33             # - finally put into standard h2xs form
34             # - added check for tick step too small
35             # - changed data validity check to permit scientific notation
36             # but this invites awful looking tick labels
37             # v 0.08 - 15 Dec 1998
38             # - added access to GD object: getGDobject() and data2pxl()
39             # v 0.09 - 26 July 1999
40             # - added custom tick labels: xTickLabels, yTickLabels
41             # - added binmode() to install test and demo script
42             # v 0.10 - 22 May 2000
43             # - added @_image_types and image_type() to use gif, jpeg or png
44             # according to local version of GD; modified draw() and _init()
45             # v 0.11 - 04 April 2001
46             # - fixed bug in draw() to enable jpeg's
47              
48             package Chart::Plot;
49              
50             $Chart::Plot::VERSION = '0.11';
51              
52 1     1   2447 use GD;
  0            
  0            
53             use strict;
54              
55              
56             #==================#
57             # class variables #
58             #==================#
59              
60             # list of image types supported by GD, currently jpeg, png or gif,
61             # depending on GD version; initialized in _init()
62             my @_image_types = ();
63              
64              
65              
66             #==================#
67             # public methods #
68             #==================#
69              
70             # usage: $plot = new Chart::Plot(); # default 400 by 300 pixels or
71             # $plot = new Chart::Plot(640, 480);
72             sub new {
73             my $class = shift;
74             my $self = {};
75             bless $self, $class;
76             $self->_init (@_);
77              
78             return $self;
79             }
80              
81             sub setData {
82             my $self = shift;
83             my ($arrayref1, $arrayref2, $style) = @_;
84             my ($arrayref, $i);
85              
86             if (ref $arrayref2) { # passing data as two data arrays (x0 ...) (y0 ...)
87              
88             unless ($#$arrayref1 = $#$arrayref2) { # error checking
89             $self->{'_errorMessage'} = "The dataset does not contain an equal number of x and y values.";
90             return 0;
91             }
92              
93             # check whether data are numeric
94             # and construct a single flat array
95             for ($i=0; $i<=$#$arrayref1; $i++) {
96              
97             if ($$arrayref1[$i] !~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/) {
98             # if ($$arrayref1[$i] =~ /[^\d\.-]/) {
99             $self->{'_errorMessage'} = "The data element $$arrayref1[$i] is non-numeric.";
100             return 0;
101             }
102             if ($$arrayref2[$i] !~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/) {
103             # if ($$arrayref2[$i] =~ /[^\d\.-]/) {
104             $self->{'_errorMessage'} = "The data element $$arrayref2[$i] is non-numeric.";
105             return 0;
106             }
107              
108             # construct a flat array
109             $$arrayref[2*$i] = $$arrayref1[$i];
110             $$arrayref[2*$i+1] = $$arrayref2[$i];
111             }
112              
113             } else { # passing data as a single flat data array (x0 y0 ...)
114              
115             $arrayref = $arrayref1;
116             $style = $arrayref2;
117              
118             # check whether array is unbalanced
119             if ($#$arrayref % 2 == 0) {
120             $self->{'_errorMessage'} = "The dataset does not contain an equal number of x and y values.";
121             return 0;
122             }
123              
124             # check whether data are numeric
125             for ($i=0; $i<=$#$arrayref; $i++) {
126             if ($$arrayref[$i] !~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/) {
127             # if ($$arrayref1[$i] =~ /[^\d\.-]/) {
128             $self->{'_errorMessage'} = "The data element $$arrayref[$i] is non-numeric.";
129             return 0;
130             }
131             }
132             }
133              
134             # record the dataset
135             my $label = ++$self->{'_numDataSets'};
136             $self->{'_data'}->{$label} = $arrayref;
137             $self->{'_dataStyle'}->{$label} = ($style ? $style : 'linespoints');
138              
139             $self->{'_validMinMax'} = 0; # invalidate any prior min-max calculations
140             return $label;
141             }
142              
143             sub error {
144             my $self = shift;
145             return $self->{'_errorMessage'};
146             }
147              
148             sub setGraphOptions {
149             my $self = shift;
150             my %hash = @_;
151              
152             for (keys (%hash)) {
153             $self->{"_$_"} = $hash{$_};
154              
155             # check tick labels for non-numeric positions
156             if (/^(x|y)TickLabels$/) {
157             my $xory = $1;
158             foreach ( keys %{$hash{$_}} ) {
159             unless (/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/) {
160             $self->{'_errorMessage'} =
161             "The $xory axis tick label position $_ is non-numeric.";
162             return 0;
163             }
164             }
165             }
166             }
167             return 1;
168             }
169              
170             sub getBounds {
171             my $self = shift;
172             $self->_getMinMax() unless $self->{'_validMinMax'};
173              
174             return ($self->{'_xmin'}, $self->{'_ymin'},
175             $self->{'_xmax'}, $self->{'_ymax'});
176             }
177              
178             sub image_type {
179             return (wantarray ? @_image_types : $_image_types[0]);
180             }
181              
182             sub draw {
183             my $self = shift;
184              
185              
186             # draw stuff in the GD object
187             $self->_getMinMax() unless $self->{'_validMinMax'};
188             $self->_drawTitle() if $self->{'_title'}; # vert offset may be increased
189             $self->_drawAxes();
190             $self->_drawData();
191              
192             # construct the image and return it.
193             # $_image_types[0] is the supported GD format, gif or png or jpeg
194             # Damien says no good way around this temp variable
195             if ($_[0]) { # image type argument
196             unless ( $self->{'_im'}->can($_[0]) ) {
197             $self->{'_errorMessage'} = "The image format $_[0] is not supported by this version $GD::VERSION of GD";
198             return undef;
199             }
200              
201             $_ = $_[0]; # forgot these in ver 0.10
202             return $self->{'_im'}->$_(); # an embarrassment
203              
204             } else {
205             $_ = $_image_types[0];
206             return $self->{'_im'}->$_();
207             }
208             }
209              
210             sub getGDobject {
211             my $self = shift;
212              
213             if (wantarray) {
214             return ( $self->{'_im'},
215             $self->{'_black'}, $self->{'_white'},
216             $self->{'_red'}, $self->{'_green'}, $self->{'_blue'},
217             );
218             } else {
219             return $self->{'_im'};
220             }
221             }
222              
223             sub data2pxl {
224             my $self = shift;
225             my @data = @_;
226              
227             # calculate required translation parameters
228             $self->_getMinMax() unless $self->{'_validMinMax'};
229              
230             return $self->_data2pxl (@data);
231             }
232              
233              
234             #===================#
235             # private methods #
236             #===================#
237              
238             # initialization
239             # this contains a record of all private data except class variables, up top
240             sub _init {
241             my $self = shift;
242              
243             # create an image object
244             if ($#_ == 1) {
245             $self->{'_im'} = new GD::Image($_[0], $_[1]);
246             $self->{'_imx'} = $_[0];
247             $self->{'_imy'} = $_[1];
248             }
249             else {
250             $self->{'_im'} = new GD::Image(400,300);
251             $self->{'_imx'} = 400;
252             $self->{'_imy'} = 300;
253             }
254              
255             # find format(s) supported by GD
256             unless (@_image_types) {
257             for ( qw(png gif jpeg) ) {
258             push @_image_types, $_ if $self->{'_im'}->can($_);
259             }
260             }
261              
262             # set graph offset; graph will be centered this many pixels within image
263             $self->{'_horGraphOffset'} = 50;
264             $self->{'_vertGraphOffset'} = 50;
265              
266             # create an empty hash for the datsets
267             # data sets and their styles are hashes whose keys are 1 ... _numDataSets
268             # and values are refs to flat data arrays or style strings, respectively
269             $self->{'_data'} = {};
270             $self->{'_dataStyle'} = {};
271             $self->{'_numDataSets'} = 0;
272              
273             # calculated by _getMinMax and used in translating _data2pxl()
274             $self->{'_xmin'} = 0; $self->{'_xmax'} = 0; # among all datasets
275             $self->{'_ymin'} = 0; $self->{'_ymax'} = 0;
276             $self->{'_xslope'} = 0; $self->{'_yslope'} = 0; # for _data2pxl()
277             $self->{'_ax'} = 0; $self->{'_ay'} = 0;
278             $self->{"_omx"} = 0; $self->{"_omy"} = 0; # for axis ticks
279             $self->{'_validMinMax'} = 0; # last calculated min and max still valid
280              
281             # initialize text
282             ($self->{'_horAxisLabel'}, $self->{'_vertAxisLabel'}) = ('','');
283             $self->{'_title'} = '';
284             $self->{'_errorMessage'} = '';
285              
286             # initialize custom tick labels
287             ($self->{'_xTickLabels'}, $self->{'_yTickLabels'}) = (0,0);
288              
289             # allocate some colors
290             $self->{'_white'} = $self->{'_im'}->colorAllocate(255,255,255);
291             $self->{'_black'} = $self->{'_im'}->colorAllocate(0,0,0);
292             $self->{'_red'} = $self->{'_im'}->colorAllocate(255,0,0);
293             $self->{'_blue'} = $self->{'_im'}->colorAllocate(0,0,255);
294             $self->{'_green'} = $self->{'_im'}->colorAllocate(0,255,0);
295              
296             # make the background transparent and interlaced
297             $self->{'_im'}->transparent($self->{'_white'});
298             $self->{'_im'}->interlaced('true');
299              
300             # Put a black frame around the picture
301             $self->{'_im'}->rectangle( 0, 0,
302             $self->{'_imx'}-1, $self->{'_imy'}-1,
303             $self->{'_black'});
304              
305             # undocumented: in script, use as $plotObject->{'_debugging'} = 1;
306             $self->{'_debugging'} = 0;
307             }
308              
309              
310             # sets min and max values of all data (_xmin, _ymin, _xmax, _ymax);
311             # also sets _xslope, _yslope, _ax and _ay used in _data2pxl;
312             # usage: $self->_getMinMax
313             sub _getMinMax {
314             my $self = shift;
315             my ($i, $arrayref);
316             my ($xmin, $ymin, $xmax, $ymax);
317              
318             # if no data, set arbitrary bounds
319             ($xmin, $ymin, $xmax, $ymax) = (0,0,1,1) unless keys %{$self->{'_data'}} > 0;
320              
321             # initialize to zero
322             $xmin = $xmax = $ymin = $ymax = 0;
323             # # or to first data point of an arbitrary dataset
324             # foreach (keys %{$self->{'_data'}}) {
325             # $arrayref = $self->{'_data'}->{$_};
326             # $xmin = $xmax = ($self->{'_noZeroX'} ? $$arrayref[0] : 0);
327             # $ymin = $ymax = ($self->{'_noZeroY'} ? $$arrayref[1] : 0);
328             # last; # skip any other datasets
329             # }
330              
331             # cycle through the datasets looking for min and max values
332             foreach (keys %{$self->{'_data'}}) {
333              
334             $arrayref = $self->{'_data'}->{$_};
335              
336             for ($i=0; $i<$#{$arrayref}; $i++) {
337             $xmin = ($xmin > $$arrayref[$i] ? $$arrayref[$i] : $xmin);
338             $xmax = ($xmax < $$arrayref[$i] ? $$arrayref[$i] : $xmax);
339             $i++;
340             $ymin = ($ymin > $$arrayref[$i] ? $$arrayref[$i] : $ymin);
341             $ymax = ($ymax < $$arrayref[$i] ? $$arrayref[$i] : $ymax);
342             }
343             }
344              
345             # set axes data ranges as decimal order of magnitude of widest dataset
346             ($self->{'_xmin'}, $self->{'_xmax'}) = $self->_getOM ('x', $xmin,$xmax);
347             ($self->{'_ymin'}, $self->{'_ymax'}) = $self->_getOM ('y', $ymin,$ymax);
348              
349             # calculate conversion constants for _data2pxl()
350             $self->{'_xslope'} = ($self->{'_imx'} - 2 * $self->{'_horGraphOffset'})
351             / ($self->{'_xmax'} - $self->{'_xmin'});
352             $self->{'_yslope'} = ($self->{'_imy'} - 2 * $self->{'_vertGraphOffset'})
353             / ($self->{'_ymax'} - $self->{'_ymin'});
354              
355             $self->{'_ax'} = $self->{'_horGraphOffset'};
356             $self->{'_ay'} = $self->{'_imy'} - $self->{'_vertGraphOffset'};
357              
358             $self->{'_validMinMax'} = 1;
359              
360             print STDERR
361             "_getMinMax(): ($self->{'_omx'}, $self->{'_omy'}) "
362             . "($xmin,$xmax) ($ymin,$ymax) "
363             . "($self->{'_xmin'}, $self->{'_xmax'}) "
364             . "($self->{'_ymin'}, $self->{'_ymax'})\n"
365             if $self->{'_debugging'};
366             }
367              
368              
369             # returns order of magnitude (with decimal) greater than +/- min and max
370             # sets _omx or _omy used for translating _data2pxl
371             # usage: ($min, $max) = $self->_getOM ('x', $xmin, $xmax); # or ('y', $ymin, $ymax)
372             sub _getOM {
373             my $self = shift;
374             my $xory = shift;
375             my @nums = @_;
376             my ($tmp, $om) = (0,0);
377             my @sign = ();
378              
379             if ($nums[0] == 0 && $nums[1] == 0) {
380             $self->{"_om$xory"} = 1;
381             return (0,1);
382             }
383             # find the (exponential) order of magnitude eg, 1000
384             foreach (@nums) {
385             if ($_<0) {
386             push @sign, ('-1');
387             $_ = - $_;
388             } elsif ($_ == 0) {
389             push @sign, ('0');
390             next;
391             } else {
392             push @sign, ('1');
393             }
394              
395             $tmp = 10 ** (int (log($_) / log(10))); # 1, 10, 100, ... less than $_
396             $om = ( $tmp>$om ? $tmp : $om );
397             }
398             $self->{"_om$xory"} = $om;
399              
400             # return the decimal order of magnitude eg, 7000
401             # epsilon adjustment in case om equals min or max
402             return (0,0) if $om == 0; # to prevent divide by zero
403             return ( $om * (int(($_[0]-0.00001*$sign[0])/$om) + $sign[0]),
404             $om * (int(($_[1]-0.00001*$sign[1])/$om) + $sign[1])
405             );
406             }
407              
408              
409              
410             # draws all the datasets in $self->{'_data'}
411             # usage: $self->_drawData()
412             sub _drawData {
413             my $self = shift;
414             my ($i, $num, $px, $py, $prevpx, $prevpy, $dataSetLabel, $color);
415              
416             foreach $dataSetLabel (keys %{$self->{'_data'}}) {
417              
418             # get color
419             if ( $self->{'_dataStyle'}->{$dataSetLabel} =~ /((red)|(blue)|(green))/i ) {
420             $color = "_$1";
421             $color =~ tr/A-Z/a-z/;
422             } else {
423             $color = '_black';
424             }
425              
426             # draw the first point
427             ($px, $py) = $self->_data2pxl (
428             $self->{'_data'}->{$dataSetLabel} [0],
429             $self->{'_data'}->{$dataSetLabel} [1]
430             );
431             $self->{'_im'}->arc($px, $py,4,4,0,360,$self->{$color})
432             unless $self->{'_dataStyle'}->{$dataSetLabel} =~ /nopoint/i;
433              
434             ($prevpx, $prevpy) = ($px, $py);
435              
436             # debugging
437             if ($self->{'_debugging'}) {
438             $self->{'_im'}->string(gdSmallFont,$px,$py-10,
439             "0($px,$py)",$self->{$color});
440             print STDERR "pxldata: 0 ($px, $py)";
441             }
442              
443             # draw the rest of the points and lines
444             $num = @{ $self->{'_data'}->{$dataSetLabel} };
445             for ($i=2; $i<$num; $i+=2) {
446              
447             # get next point
448             ($px, $py) = $self->_data2pxl (
449             $self->{'_data'}->{$dataSetLabel}[$i],
450             $self->{'_data'}->{$dataSetLabel}[$i+1]
451             );
452              
453             # draw point, maybe
454             $self->{'_im'}->arc($px, $py,4,4,0,360,$self->{$color})
455             unless $self->{'_dataStyle'}->{$dataSetLabel} =~ /nopoint/i;
456              
457             # draw line from previous point, maybe
458             if ($self->{'_dataStyle'}->{$dataSetLabel} =~ /dashed/) {
459             $self->{'_im'}->dashedLine($prevpx, $prevpy, $px, $py, $self->{$color});
460             } elsif ($self->{'_dataStyle'}->{$dataSetLabel} =~ /noline/i) {
461             next;
462             } else { # default to solid line
463             $self->{'_im'}->line($prevpx, $prevpy, $px, $py, $self->{$color});
464             }
465              
466             ($prevpx, $prevpy) = ($px, $py);
467              
468             # debugging
469             if ($self->{'_debugging'}) {
470             $self->{'_im'}->string(gdSmallFont,$px-10,$py+10,
471             "$i($px,$py)",$self->{$color});
472             print STDERR "$i ($px, $py)";
473             }
474             }
475             }
476             }
477              
478              
479              
480             # translate a data point to the nearest pixel point within the graph
481             # usage: (px,py) = $self->_data2pxl (x,y)
482             sub _data2pxl {
483             my $self = shift;
484             my ($x, $y) = @_;
485              
486             return ( int ( $self->{'_ax'}
487             + ($x - $self->{'_xmin'}) * $self->{'_xslope'} ),
488             int ( $self->{'_ay'}
489             - ($y - $self->{'_ymin'}) * $self->{'_yslope'} )
490             );
491             }
492              
493              
494              
495             # draw the axes, axis labels, ticks and tick labels
496             # usage: $self->_drawAxes
497             sub _drawAxes {
498             # axes run from data points: x -- ($xmin,0) ($xmax,0);
499             # y -- (0,$ymin) (0,$ymax);
500             # these mins and maxes are decimal orders of magnitude bounding the data
501              
502             my $self = shift;
503             my ($w,$h) = (gdSmallFont->width, gdSmallFont->height);
504              
505             ### horizontal axis
506             my ($p1x, $p1y) = $self->_data2pxl ($self->{'_xmin'}, 0);
507             my ($p2x, $p2y) = $self->_data2pxl ($self->{'_xmax'}, 0);
508             $self->{'_im'}->line($p1x, $p1y, $p2x, $p2y, $self->{'_black'});
509              
510             ### axis label
511             my $len = $w * length ($self->{'_horAxisLabel'});
512             my $xStart = ($p2x+$len/2 > $self->{'_imx'}-10) # center under right end of axis
513             ? ($self->{'_imx'}-10-$len) : ($p2x-$len/2); # or right justify
514             $self->{'_im'}->string (gdSmallFont, $xStart, $p2y+3*$h/2,
515             $self->{'_horAxisLabel'},
516             $self->{'_black'});
517              
518             print STDERR "\nHor: p1 ($p1x, $p1y) p2 ($p2x, $p2y)\n"
519             if $self->{'_debugging'};
520              
521             ### vertical axis
522             ($p1x, $p1y) = $self->_data2pxl (0, $self->{'_ymin'});
523             ($p2x, $p2y) = $self->_data2pxl (0, $self->{'_ymax'});
524             $self->{'_im'}->line($p1x, $p1y, $p2x, $p2y, $self->{'_black'});
525              
526             ### axis label
527             $xStart = $p2x - length ($self->{'_vertAxisLabel'}) * $w / 2;
528             $self->{'_im'}->string (gdSmallFont, ($xStart>10 ? $xStart : 10), $p2y - 2*$h,
529             $self->{'_vertAxisLabel'},
530             $self->{'_black'});
531            
532             print STDERR "Ver: p1 ($p1x, $p1y) p2 ($p2x, $p2y)\n"
533             if $self->{'_debugging'};
534              
535             ###
536             ### draw axis ticks and tick labels
537             ###
538             my ($i,$px,$py, $step);
539            
540              
541             ###
542             ### horizontal
543             ###
544             # if horizontal custom tick labels
545             if ($self->{'_xTickLabels'}) {
546            
547             # a hashref with horizontal data point and label
548             # example: %{$self->{'_xTickLabels'} = (10 => 'Ten', 20 => 'Twenty', ...)
549             foreach ( keys %{$self->{'_xTickLabels'}} ) {
550            
551             ($px,$py) = $self->_data2pxl($_, 0);
552             $self->{'_im'}->line($px, $py-2, $px, $py+2, $self->{'_black'});
553             $self->{'_im'}->string ( gdSmallFont,
554             $px-length( ${$self-> {'_xTickLabels'}}{$_} ) * $w/2,
555             $py+$h/2,
556             ${$self->{'_xTickLabels'}}{$_},
557             $self->{'_black'}
558             );
559             }
560            
561             } else {
562              
563             # horizontal step calculation
564             $step = $self->{'_omx'};
565             # step too large
566             $step /= 2 if ($self->{'_xmax'} - $self->{'_xmin'}) / $step < 6;
567             # once again. A poor hack for case om = max.
568             $step /= 2 if ($self->{'_xmax'} - $self->{'_xmin'}) / $step < 6;
569             # step too small. As long as we are doing poor hacks
570             $step *= 2 if ($self->{'_xmax'} - $self->{'_xmin'}) / $step > 12;
571            
572             for ($i=$self->{'_xmin'}; $i <= $self->{'_xmax'}; $i+=$step ) {
573             ($px,$py) = $self->_data2pxl($i, 0);
574             $self->{'_im'}->line($px, $py-2, $px, $py+2, $self->{'_black'});
575             $self->{'_im'}->string (gdSmallFont,
576             $px-length($i)*$w/2, $py+$h/2,
577             $i, $self->{'_black'}) unless $i == 0;
578             }
579             print STDERR "Horstep: $step ($self->{'_xmax'} - $self->{'_xmin'})/$self->{'_omx'})\n"
580             if $self->{'_debugging'};
581             }
582              
583             ###
584             ### vertical
585             ###
586             if ($self->{'_yTickLabels'}) {
587             foreach ( keys %{$self->{'_yTickLabels'}} ) {
588             ($px,$py) = $self->_data2pxl(0, $_);
589             $self->{'_im'}->line($px-2, $py, $px+2, $py, $self->{'_black'});
590             $self->{'_im'}->string ( gdSmallFont,
591             $px-(1+length( ${$self->{'_yTickLabels'}}{$_} )) * $h/2,
592             $py-$h/2,
593             ${$self->{'_yTickLabels'}}{$_},
594             $self->{'_black'});
595             }
596             } else {
597             $step = $self->{'_omy'};
598             $step /= 2 if ($self->{'_ymax'} - $self->{'_ymin'}) / $step < 6;
599             $step /= 2 if ($self->{'_ymax'} - $self->{'_ymin'}) / $step < 6;
600             $step *= 2 if ($self->{'_ymax'} - $self->{'_ymin'}) / $step > 12;
601              
602             for ($i=$self->{'_ymin'}; $i <= $self->{'_ymax'}; $i+=$step ) {
603             ($px,$py) = $self->_data2pxl (0, $i);
604             $self->{'_im'}->line($px-2, $py, $px+2, $py, $self->{'_black'});
605             $self->{'_im'}->string (gdSmallFont,
606             $px-5-length($i)*$w, $py-$h/2,
607             $i, $self->{'_black'}) unless $i == 0;
608             }
609             print STDERR "Verstep: $step ($self->{'_ymax'} - $self->{'_ymin'})/$self->{'_omy'})\n"
610             if $self->{'_debugging'};
611             }
612             }
613              
614              
615             sub _drawTitle {
616             my $self = shift;
617             my ($w,$h) = (gdMediumBoldFont->width, gdMediumBoldFont->height);
618              
619             # increase vert offset and recalculate conversion constants for _data2pxl()
620             $self->{'_vertGraphOffset'} += 2*$h;
621              
622             $self->{'_xslope'} = ($self->{'_imx'} - 2 * $self->{'_horGraphOffset'})
623             / ($self->{'_xmax'} - $self->{'_xmin'});
624             $self->{'_yslope'} = ($self->{'_imy'} - 2 * $self->{'_vertGraphOffset'})
625             / ($self->{'_ymax'} - $self->{'_ymin'});
626              
627             $self->{'_ax'} = $self->{'_horGraphOffset'};
628             $self->{'_ay'} = $self->{'_imy'} - $self->{'_vertGraphOffset'};
629              
630              
631             # centered below chart
632             my ($px,$py) = ($self->{'_imx'}/2, # $self->{'_vertGraphOffset'}/2);
633             $self->{'_imy'} - $self->{'_vertGraphOffset'}/2);
634              
635             ($px,$py) = ($px - length ($self->{'_title'}) * $w/2, $py+$h/2);
636             $self->{'_im'}->string (gdMediumBoldFont, $px, $py,
637             $self->{'_title'},
638             $self->{'_black'});
639             }
640              
641             1;
642              
643             __END__