From: Patrick Michaud <pmichaud@u.washington.edu>
[imager-graph.git] / lib / Imager / Graph / Vertical.pm
1 package Imager::Graph::Vertical;
2
3 =head1 NAME
4
5   Imager::Graph::Vertical- A super class for line/bar/column charts
6
7 =cut
8
9 use strict;
10 use vars qw(@ISA);
11 use Imager::Graph;
12 @ISA = qw(Imager::Graph);
13
14 use constant STARTING_MIN_VALUE => 99999;
15 =over 4
16
17 =item add_data_series(\@data, $series_name)
18
19 Add a data series to the graph, of the default type.
20
21 =cut
22
23 sub add_data_series {
24   my $self = shift;
25   my $data_ref = shift;
26   my $series_name = shift;
27
28   my $series_type = $self->_get_default_series_type();
29   $self->_add_data_series($series_type, $data_ref, $series_name);
30
31   return;
32 }
33
34 =item add_column_data_series(\@data, $series_name)
35
36 Add a column data series to the graph.
37
38 =cut
39
40 sub add_column_data_series {
41   my $self = shift;
42   my $data_ref = shift;
43   my $series_name = shift;
44
45   $self->_add_data_series('column', $data_ref, $series_name);
46
47   return;
48 }
49
50 =item add_stacked_column_data_series(\@data, $series_name)
51
52 Add a stacked column data series to the graph.
53
54 =cut
55
56 sub add_stacked_column_data_series {
57   my $self = shift;
58   my $data_ref = shift;
59   my $series_name = shift;
60
61   $self->_add_data_series('stacked_column', $data_ref, $series_name);
62
63   return;
64 }
65
66 =item add_line_data_series(\@data, $series_name)
67
68 Add a line data series to the graph.
69
70 =cut
71
72 sub add_line_data_series {
73   my $self = shift;
74   my $data_ref = shift;
75   my $series_name = shift;
76
77   $self->_add_data_series('line', $data_ref, $series_name);
78
79   return;
80 }
81
82 =item set_y_max($value)
83
84 Sets the maximum y value to be displayed.  This will be ignored if the y_max is lower than the highest value.
85
86 =cut
87
88 sub set_y_max {
89   $_[0]->{'custom_style'}->{'y_max'} = $_[1];
90 }
91
92 =item set_y_min($value)
93
94 Sets the minimum y value to be displayed.  This will be ignored if the y_min is higher than the lowest value.
95
96 =cut
97
98 sub set_y_min {
99   $_[0]->{'custom_style'}->{'y_min'} = $_[1];
100 }
101
102 =item set_range_padding($percentage)
103
104 Sets the padding to be used, as a percentage.  For example, if your data ranges from 0 to 10, and you have a 20 percent padding, the y axis will go to 12.
105
106 Defaults to 10.  This attribute is ignored for positive numbers if set_y_max() has been called, and ignored for negative numbers if set_y_min() has been called.
107
108 =cut
109
110 sub set_range_padding {
111   $_[0]->{'custom_style'}->{'range_padding'} = $_[1];
112 }
113
114 =item set_negative_background($color)
115
116 Sets the background color used below the x axis.
117
118 =cut
119
120 sub set_negative_background {
121   $_[0]->{'custom_style'}->{'negative_bg'} = $_[1];
122 }
123
124 =item draw()
125
126 Draw the graph
127
128 =cut
129
130 sub draw {
131   my ($self, %opts) = @_;
132
133   if (!$self->_valid_input()) {
134     return;
135   }
136
137   $self->_style_setup(\%opts);
138
139   my $style = $self->{_style};
140
141   my $img = $self->_get_image()
142     or return;
143
144   my @image_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
145   $self->_set_image_box(\@image_box);
146
147   my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
148   $self->_draw_legend(\@chart_box);
149   if ($style->{title}{text}) {
150     $self->_draw_title($img, \@chart_box)
151       or return;
152   }
153
154   # Scale the graph box down to the widest graph that can cleanly hold the # of columns.
155   return unless $self->_get_data_range();
156   $self->_remove_tics_from_chart_box(\@chart_box);
157   my $column_count = $self->_get_column_count();
158
159   my $width = $self->_get_number('width');
160   my $height = $self->_get_number('height');
161
162   my $graph_width = $chart_box[2] - $chart_box[0];
163   my $graph_height = $chart_box[3] - $chart_box[1];
164
165   my $col_width = int(($graph_width - 1) / $column_count) -1;
166   $graph_width = $col_width * $column_count + 1;
167
168   my $tic_count = $self->_get_y_tics();
169   my $tic_distance = int(($graph_height-1) / ($tic_count - 1));
170   $graph_height = $tic_distance * ($tic_count - 1);
171
172   my $bottom = $chart_box[1];
173   my $left   = $chart_box[0];
174
175   $self->{'_style'}{'graph_width'} = $graph_width;
176   $self->{'_style'}{'graph_height'} = $graph_height;
177
178   my @graph_box = ($left, $bottom, $left + $graph_width, $bottom + $graph_height);
179   $self->_set_graph_box(\@graph_box);
180
181   $img->box(
182             color   => $self->_get_color('outline.line'),
183             xmin    => $left,
184             xmax    => $left+$graph_width,
185             ymin    => $bottom,
186             ymax    => $bottom+$graph_height,
187             );
188
189   $img->box(
190             color   => $self->_get_color('bg'),
191             xmin    => $left + 1,
192             xmax    => $left+$graph_width - 1,
193             ymin    => $bottom + 1,
194             ymax    => $bottom+$graph_height-1 ,
195             filled  => 1,
196             );
197
198   my $min_value = $self->_get_min_value();
199   my $max_value = $self->_get_max_value();
200   my $value_range = $max_value - $min_value;
201
202   my $zero_position;
203   if ($value_range) {
204     $zero_position =  $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height-1);
205   }
206
207   if ($min_value < 0) {
208     $img->box(
209             color   => $self->_get_color('negative_bg'),
210             xmin    => $left + 1,
211             xmax    => $left+$graph_width- 1,
212             ymin    => $zero_position,
213             ymax    => $bottom+$graph_height - 1,
214             filled  => 1,
215     );
216     $img->line(
217             x1 => $left+1,
218             y1 => $zero_position,
219             x2 => $left + $graph_width,
220             y2 => $zero_position,
221             color => $self->_get_color('outline.line'),
222     );
223   }
224
225   if ($self->_get_data_series()->{'stacked_column'}) {
226     $self->_draw_stacked_columns();
227   }
228   if ($self->_get_data_series()->{'column'}) {
229     $self->_draw_columns();
230   }
231   if ($self->_get_data_series()->{'line'}) {
232     $self->_draw_lines();
233   }
234
235   if ($self->_get_y_tics()) {
236     $self->_draw_y_tics();
237   }
238   if ($self->_get_labels()) {
239     $self->_draw_x_tics();
240   }
241
242   return $self->_get_image();
243 }
244
245 sub _get_data_range {
246   my $self = shift;
247
248   my $max_value = 0;
249   my $min_value = 0;
250   my $column_count = 0;
251
252   my ($sc_min, $sc_max, $sc_cols) = $self->_get_stacked_column_range();
253   my ($c_min, $c_max, $c_cols) = $self->_get_column_range();
254   my ($l_min, $l_max, $l_cols) = $self->_get_line_range();
255
256   # These are side by side...
257   $sc_cols += $c_cols;
258
259   $min_value = $self->_min(STARTING_MIN_VALUE, $sc_min, $c_min, $l_min);
260   $max_value = $self->_max(0, $sc_max, $c_max, $l_max);
261
262   my $config_min = $self->_get_number('y_min');
263   my $config_max = $self->_get_number('y_max');
264
265   if (defined $config_max && $config_max < $max_value) {
266     $config_max = undef;
267   }
268   if (defined $config_min && $config_min > $min_value) {
269     $config_min = undef;
270   }
271
272   my $range_padding = $self->_get_number('range_padding');
273   if (defined $config_min) {
274     $min_value = $config_min;
275   }
276   else {
277     if ($min_value > 0) {
278       $min_value = 0;
279     }
280     if ($range_padding && $min_value < 0) {
281       my $difference = $min_value * $range_padding / 100;
282       if ($min_value < -1 && $difference > -1) {
283         $difference = -1;
284       }
285       $min_value += $difference;
286     }
287   }
288   if (defined $config_max) {
289     $max_value = $config_max;
290   }
291   else {
292     if ($range_padding && $max_value > 0) {
293       my $difference = $max_value * $range_padding / 100;
294       if ($max_value > 1 && $difference < 1) {
295         $difference = 1;
296       }
297       $max_value += $difference;
298     }
299   }
300   $column_count = $self->_max(0, $sc_cols, $l_cols);
301
302   if ($self->_get_number('automatic_axis')) {
303     # In case this was set via a style, and not by the api method
304     eval { require Chart::Math::Axis; };
305     if ($@) {
306       return $self->_error("Can't use automatic_axis - $@");
307     }
308
309     my $axis = Chart::Math::Axis->new();
310     $axis->add_data($min_value, $max_value);
311     $max_value = $axis->top;
312     $min_value = $axis->bottom;
313     my $ticks     = $axis->ticks;
314     # The +1 is there because we have the bottom tick as well
315     $self->set_y_tics($ticks+1);
316   }
317
318   $self->_set_max_value($max_value);
319   $self->_set_min_value($min_value);
320   $self->_set_column_count($column_count);
321
322   return 1;
323 }
324
325 sub _min {
326   my $self = shift;
327   my $min = shift;
328
329   foreach my $value (@_) {
330     next unless defined $value;
331     if ($value < $min) { $min = $value; }
332   }
333   return $min;
334 }
335
336 sub _max {
337   my $self = shift;
338   my $min = shift;
339
340   foreach my $value (@_) {
341     next unless defined $value;
342     if ($value > $min) { $min = $value; }
343   }
344   return $min;
345 }
346
347 sub _get_line_range {
348   my $self = shift;
349   my $series = $self->_get_data_series()->{'line'};
350   return (undef, undef, 0) unless $series;
351
352   my $max_value = 0;
353   my $min_value = STARTING_MIN_VALUE;
354   my $column_count = 0;
355
356   my @series = @{$series};
357   foreach my $series (@series) {
358     my @data = @{$series->{'data'}};
359
360     if (scalar @data > $column_count) {
361       $column_count = scalar @data;
362     }
363
364     foreach my $value (@data) {
365       if ($value > $max_value) { $max_value = $value; }
366       if ($value < $min_value) { $min_value = $value; }
367     }
368   }
369
370   return ($min_value, $max_value, $column_count);
371 }
372
373 sub _get_column_range {
374   my $self = shift;
375
376   my $series = $self->_get_data_series()->{'column'};
377   return (undef, undef, 0) unless $series;
378
379   my $max_value = 0;
380   my $min_value = STARTING_MIN_VALUE;
381   my $column_count = 0;
382
383   my @series = @{$series};
384   foreach my $series (@series) {
385     my @data = @{$series->{'data'}};
386
387     foreach my $value (@data) {
388       $column_count++;
389       if ($value > $max_value) { $max_value = $value; }
390       if ($value < $min_value) { $min_value = $value; }
391     }
392   }
393
394   return ($min_value, $max_value, $column_count);
395 }
396
397 sub _get_stacked_column_range {
398   my $self = shift;
399
400   my $max_value = 0;
401   my $min_value = STARTING_MIN_VALUE;
402   my $column_count = 0;
403
404   return (undef, undef, 0) unless $self->_get_data_series()->{'stacked_column'};
405   my @series = @{$self->_get_data_series()->{'stacked_column'}};
406
407   my @max_entries;
408   my @min_entries;
409   for (my $i = scalar @series - 1; $i >= 0; $i--) {
410     my $series = $series[$i];
411     my $data = $series->{'data'};
412
413     for (my $i = 0; $i < scalar @$data; $i++) {
414       my $value = 0;
415       if ($data->[$i] > 0) {
416         $value = $data->[$i] + ($max_entries[$i] || 0);
417         $data->[$i] = $value;
418         $max_entries[$i] = $value;
419       }
420       elsif ($data->[$i] < 0) {
421         $value = $data->[$i] + ($min_entries[$i] || 0);
422         $data->[$i] = $value;
423         $min_entries[$i] = $value;
424       }
425       if ($value > $max_value) { $max_value = $value; }
426       if ($value < $min_value) { $min_value = $value; }
427     }
428     if (scalar @$data > $column_count) {
429       $column_count = scalar @$data;
430     }
431   }
432
433   return ($min_value, $max_value, $column_count);
434 }
435
436 sub _draw_legend {
437   my $self = shift;
438   my $chart_box = shift;
439   my $style = $self->{'_style'};
440
441   my @labels;
442   my $img = $self->_get_image();
443   if (my $series = $self->_get_data_series()->{'stacked_column'}) {
444     push @labels, map { $_->{'series_name'} } @$series;
445   }
446   if (my $series = $self->_get_data_series()->{'column'}) {
447     push @labels, map { $_->{'series_name'} } @$series;
448   }
449   if (my $series = $self->_get_data_series()->{'line'}) {
450     push @labels, map { $_->{'series_name'} } @$series;
451   }
452
453   if ($style->{features}{legend} && (scalar @labels)) {
454     $self->SUPER::_draw_legend($self->_get_image(), \@labels, $chart_box)
455       or return;
456   }
457   return;
458 }
459
460 sub _draw_flat_legend {
461   return 1;
462 }
463
464 sub _draw_lines {
465   my $self = shift;
466   my $style = $self->{'_style'};
467
468   my $img = $self->_get_image();
469
470   my $max_value = $self->_get_max_value();
471   my $min_value = $self->_get_min_value();
472   my $column_count = $self->_get_column_count();
473
474   my $value_range = $max_value - $min_value;
475
476   my $width = $self->_get_number('width');
477   my $height = $self->_get_number('height');
478
479   my $graph_width = $self->_get_number('graph_width');
480   my $graph_height = $self->_get_number('graph_height');
481
482   my $line_series = $self->_get_data_series()->{'line'};
483   my $series_counter = $self->_get_series_counter() || 0;
484
485   my $has_columns = (defined $self->_get_data_series()->{'column'} || $self->_get_data_series->{'stacked_column'}) ? 1 : 0;
486
487   my $col_width = int($graph_width / $column_count) -1;
488
489   my $graph_box = $self->_get_graph_box();
490   my $left = $graph_box->[0] + 1;
491   my $bottom = $graph_box->[1];
492
493   my $zero_position =  $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height - 1);
494
495
496   my $line_aa = $self->_get_number("lineaa");
497   foreach my $series (@$line_series) {
498     my @data = @{$series->{'data'}};
499     my $data_size = scalar @data;
500
501     my $interval;
502     if ($has_columns) {
503       $interval = $graph_width / ($data_size);
504     }
505     else {
506       $interval = $graph_width / ($data_size - 1);
507     }
508     my $color = $self->_data_color($series_counter);
509
510     # We need to add these last, otherwise the next line segment will overwrite half of the marker
511     my @marker_positions;
512     for (my $i = 0; $i < $data_size - 1; $i++) {
513       my $x1 = $left + $i * $interval;
514       my $x2 = $left + ($i + 1) * $interval;
515
516       $x1 += $has_columns * $interval / 2;
517       $x2 += $has_columns * $interval / 2;
518
519       my $y1 = $bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height;
520       my $y2 = $bottom + ($value_range - $data[$i + 1] + $min_value)/$value_range * $graph_height;
521
522       push @marker_positions, [$x1, $y1];
523       $img->line(x1 => $x1, y1 => $y1, x2 => $x2, y2 => $y2, aa => $line_aa, color => $color) || die $img->errstr;
524     }
525
526     my $x2 = $left + ($data_size - 1) * $interval;
527     $x2 += $has_columns * $interval / 2;
528
529     my $y2 = $bottom + ($value_range - $data[$data_size - 1] + $min_value)/$value_range * $graph_height;
530
531     push @marker_positions, [$x2, $y2];
532     foreach my $position (@marker_positions) {
533       $self->_draw_line_marker($position->[0], $position->[1], $series_counter);
534     }
535     $series_counter++;
536   }
537
538   $self->_set_series_counter($series_counter);
539   return;
540 }
541
542 sub _line_marker {
543   my ($self, $index) = @_;
544
545   my $markers = $self->{'_style'}{'line_markers'};
546   if (!$markers) {
547     return;
548   }
549   my $marker = $markers->[$index % @$markers];
550
551   return $marker;
552 }
553
554 sub _draw_line_marker {
555   my $self = shift;
556   my ($x1, $y1, $series_counter) = @_;
557
558   my $img = $self->_get_image();
559
560   my $style = $self->_line_marker($series_counter);
561   return unless $style;
562
563   my $type = $style->{'shape'};
564   my $radius = $style->{'radius'};
565
566   my $line_aa = $self->_get_number("lineaa");
567   my $fill_aa = $self->_get_number("fill.aa");
568   if ($type eq 'circle') {
569     my @fill = $self->_data_fill($series_counter, [$x1 - $radius, $y1 - $radius, $x1 + $radius, $y1 + $radius]);
570     $img->circle(x => $x1, y => $y1, r => $radius, aa => $fill_aa, filled => 1, @fill);
571   }
572   elsif ($type eq 'square') {
573     my @fill = $self->_data_fill($series_counter, [$x1 - $radius, $y1 - $radius, $x1 + $radius, $y1 + $radius]);
574     $img->box(xmin => $x1 - $radius, ymin => $y1 - $radius, xmax => $x1 + $radius, ymax => $y1 + $radius, @fill);
575   }
576   elsif ($type eq 'diamond') {
577     # The gradient really doesn't work for diamond
578     my $color = $self->_data_color($series_counter);
579     $img->polygon(
580         points => [
581                     [$x1 - $radius, $y1],
582                     [$x1, $y1 + $radius],
583                     [$x1 + $radius, $y1],
584                     [$x1, $y1 - $radius],
585                   ],
586         filled => 1, color => $color, aa => $fill_aa);
587   }
588   elsif ($type eq 'triangle') {
589     # The gradient really doesn't work for triangle
590     my $color = $self->_data_color($series_counter);
591     $img->polygon(
592         points => [
593                     [$x1 - $radius, $y1 + $radius],
594                     [$x1 + $radius, $y1 + $radius],
595                     [$x1, $y1 - $radius],
596                   ],
597         filled => 1, color => $color, aa => $fill_aa);
598
599   }
600   elsif ($type eq 'x') {
601     my $color = $self->_data_color($series_counter);
602     $img->line(x1 => $x1 - $radius, y1 => $y1 -$radius, x2 => $x1 + $radius, y2 => $y1+$radius, aa => $line_aa, color => $color) || die $img->errstr;
603     $img->line(x1 => $x1 + $radius, y1 => $y1 -$radius, x2 => $x1 - $radius, y2 => $y1+$radius, aa => $line_aa, color => $color) || die $img->errstr;
604   }
605   elsif ($type eq 'plus') {
606     my $color = $self->_data_color($series_counter);
607     $img->line(x1 => $x1, y1 => $y1 -$radius, x2 => $x1, y2 => $y1+$radius, aa => $line_aa, color => $color) || die $img->errstr;
608     $img->line(x1 => $x1 + $radius, y1 => $y1, x2 => $x1 - $radius, y2 => $y1, aa => $line_aa, color => $color) || die $img->errstr;
609   }
610 }
611
612 sub _draw_columns {
613   my $self = shift;
614   my $style = $self->{'_style'};
615
616   my $img = $self->_get_image();
617
618   my $max_value = $self->_get_max_value();
619   my $min_value = $self->_get_min_value();
620   my $column_count = $self->_get_column_count();
621
622   my $value_range = $max_value - $min_value;
623
624   my $width = $self->_get_number('width');
625   my $height = $self->_get_number('height');
626
627   my $graph_width = $self->_get_number('graph_width');
628   my $graph_height = $self->_get_number('graph_height');
629
630
631   my $graph_box = $self->_get_graph_box();
632   my $left = $graph_box->[0] + 1;
633   my $bottom = $graph_box->[1];
634   my $zero_position =  int($bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height -1));
635
636   my $bar_width = int($graph_width / $column_count - 1);
637
638   my $outline_color;
639   if ($style->{'features'}{'outline'}) {
640     $outline_color = $self->_get_color('outline.line');
641   }
642
643   my $series_counter = $self->_get_series_counter() || 0;
644   my $col_series = $self->_get_data_series()->{'column'};
645
646   # This tracks the series we're in relative to the starting series - this way colors stay accurate, but the columns don't start out too far to the right.
647   my $column_series = 0;
648
649   # If there are stacked columns, non-stacked columns need to start one to the right of where they would otherwise
650   my $has_stacked_columns = (defined $self->_get_data_series()->{'stacked_column'} ? 1 : 0);
651
652   for (my $series_pos = 0; $series_pos < scalar @$col_series; $series_pos++) {
653     my $series = $col_series->[$series_pos];
654     my @data = @{$series->{'data'}};
655     my $data_size = scalar @data;
656     my $color = $self->_data_color($series_counter);
657     for (my $i = 0; $i < $data_size; $i++) {
658       my $x1 = int($left + $bar_width * (scalar @$col_series * $i + $series_pos)) + scalar @$col_series * $i + $series_pos;
659       if ($has_stacked_columns) {
660         $x1 += ($i + 1) * $bar_width + $i + 1;
661       }
662       my $x2 = $x1 + $bar_width;
663
664       my $y1 = int($bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height);
665
666       my $color = $self->_data_color($series_counter);
667
668     #  my @fill = $self->_data_fill($series_counter, [$x1, $y1, $x2, $zero_position]);
669       if ($data[$i] > 0) {
670         $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position-1, color => $color, filled => 1);
671         if ($style->{'features'}{'outline'}) {
672           $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position, color => $outline_color);
673         }
674       }
675       else {
676         $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1, color => $color, filled => 1);
677         if ($style->{'features'}{'outline'}) {
678           $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1+1, color => $outline_color);
679         }
680       }
681     }
682
683     $series_counter++;
684     $column_series++;
685   }
686   $self->_set_series_counter($series_counter);
687   return;
688 }
689
690 sub _draw_stacked_columns {
691   my $self = shift;
692   my $style = $self->{'_style'};
693
694   my $img = $self->_get_image();
695
696   my $max_value = $self->_get_max_value();
697   my $min_value = $self->_get_min_value();
698   my $column_count = $self->_get_column_count();
699   my $value_range = $max_value - $min_value;
700
701   my $graph_box = $self->_get_graph_box();
702   my $left = $graph_box->[0] + 1;
703   my $bottom = $graph_box->[1];
704
705   my $graph_width = $self->_get_number('graph_width');
706   my $graph_height = $self->_get_number('graph_height');
707
708   my $bar_width = int($graph_width / $column_count -1);
709   my $column_series = 0;
710   if (my $column_series_data = $self->_get_data_series()->{'column'}) {
711     $column_series = (scalar @$column_series_data);
712   }
713   $column_series++;
714
715   my $outline_color;
716   if ($style->{'features'}{'outline'}) {
717     $outline_color = $self->_get_color('outline.line');
718   }
719
720   my $zero_position =  $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height -1);
721   my $col_series = $self->_get_data_series()->{'stacked_column'};
722   my $series_counter = $self->_get_series_counter() || 0;
723   foreach my $series (@$col_series) {
724     my @data = @{$series->{'data'}};
725     my $data_size = scalar @data;
726     my $color = $self->_data_color($series_counter);
727     for (my $i = 0; $i < $data_size; $i++) {
728       my $x1 = int($left + $bar_width * ($column_series * $i)) + $column_series * $i;
729       my $x2 = $x1 + $bar_width;
730
731       my $y1 = $bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height;
732
733       if ($data[$i] > 0) {
734         $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position-1, color => $color, filled => 1);
735         if ($style->{'features'}{'outline'}) {
736           $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position, color => $outline_color);
737         }
738       }
739       else {
740         $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1, color => $color, filled => 1);
741         if ($style->{'features'}{'outline'}) {
742           $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1+1, color => $outline_color);
743         }
744       }
745     }
746
747     $series_counter++;
748   }
749   $self->_set_series_counter($series_counter);
750   return;
751 }
752
753 sub _add_data_series {
754   my $self = shift;
755   my $series_type = shift;
756   my $data_ref = shift;
757   my $series_name = shift;
758
759   my $graph_data = $self->{'graph_data'} || {};
760
761   my $series = $graph_data->{$series_type} || [];
762
763   push @$series, { data => $data_ref, series_name => $series_name };
764
765   $graph_data->{$series_type} = $series;
766
767   $self->{'graph_data'} = $graph_data;
768   return;
769 }
770
771 =over
772
773 =item show_horizontal_gridlines()
774
775 Shows horizontal gridlines at the y-tics.
776
777 =cut
778
779 sub show_horizontal_gridlines {
780     $_[0]->{'custom_style'}->{'horizontal_gridlines'} = 1;
781 }
782
783 =item use_automatic_axis()
784
785 Automatically scale the Y axis, based on L<Chart::Math::Axis>.  If Chart::Math::Axis isn't installed, this sets an error and returns undef.  Returns 1 if it is installed.
786
787 =cut
788
789 sub use_automatic_axis {
790   eval { require Chart::Math::Axis; };
791   if ($@) {
792     return $_[0]->_error("use_automatic_axis - $@\nCalled from ".join(' ', caller)."\n");
793   }
794   $_[0]->{'custom_style'}->{'automatic_axis'} = 1;
795   return 1;
796 }
797
798
799 =item set_y_tics($count)
800
801 Set the number of Y tics to use.  Their value and position will be determined by the data range.
802
803 =cut
804
805 sub set_y_tics {
806   $_[0]->{'y_tics'} = $_[1];
807 }
808
809 sub _get_y_tics {
810   return $_[0]->{'y_tics'} || 0;
811 }
812
813 sub _remove_tics_from_chart_box {
814   my $self = shift;
815   my $chart_box = shift;
816
817   # XXX - bad default
818   my $tic_width = $self->_get_y_tic_width() || 10;
819   my @y_tic_box = ($chart_box->[0], $chart_box->[1], $chart_box->[0] + $tic_width, $chart_box->[3]);
820
821   # XXX - bad default
822   my $tic_height = $self->_get_x_tic_height() || 10;
823   my @x_tic_box = ($chart_box->[0], $chart_box->[3] - $tic_height, $chart_box->[2], $chart_box->[3]);
824
825   $self->_remove_box($chart_box, \@y_tic_box);
826   $self->_remove_box($chart_box, \@x_tic_box);
827
828   # If there's no title, the y-tics will be part off-screen.  Half of the x-tic height should be more than sufficient.
829   my @y_tic_tops = ($chart_box->[0], $chart_box->[1], $chart_box->[2], $chart_box->[1] + int($tic_height / 2));
830   $self->_remove_box($chart_box, \@y_tic_tops);
831
832 }
833
834 sub _get_y_tic_width{
835   my $self = shift;
836   my $min = $self->_get_min_value();
837   my $max = $self->_get_max_value();
838   my $tic_count = $self->_get_y_tics();
839
840   my $img = $self->_get_image();
841   my $graph_box = $self->_get_graph_box();
842   my $image_box = $self->_get_image_box();
843
844   my $interval = ($max - $min) / ($tic_count - 1);
845
846   my %text_info = $self->_text_style('legend')
847     or return;
848
849   my $max_width = 0;
850   for my $count (0 .. $tic_count - 1) {
851     my $value = sprintf("%.2f", ($count*$interval)+$min);
852
853     my @box = $self->_text_bbox($value, 'legend');
854     my $width = $box[2] - $box[0];
855
856     # For the tic width
857     $width += 10;
858     if ($width > $max_width) {
859       $max_width = $width;
860     }
861   }
862
863   return $max_width;
864 }
865
866 sub _get_x_tic_height {
867   my $self = shift;
868
869   my $labels = $self->_get_labels();
870
871   my $tic_count = (scalar @$labels) - 1;
872
873   my %text_info = $self->_text_style('legend')
874     or return;
875
876   my $max_height = 0;
877   for my $count (0 .. $tic_count) {
878     my $label = $labels->[$count];
879
880     my @box = $self->_text_bbox($label, 'legend');
881
882     my $height = $box[3] - $box[1];
883
884     # Padding + the tic
885     $height += 10;
886     if ($height > $max_height) {
887       $max_height = $height;
888     }
889   }
890
891   return $max_height;
892 }
893
894 sub _draw_y_tics {
895   my $self = shift;
896   my $min = $self->_get_min_value();
897   my $max = $self->_get_max_value();
898   my $tic_count = $self->_get_y_tics();
899
900   my $img = $self->_get_image();
901   my $graph_box = $self->_get_graph_box();
902   my $image_box = $self->_get_image_box();
903
904   my $interval = ($max - $min) / ($tic_count - 1);
905
906   my %text_info = $self->_text_style('legend')
907     or return;
908
909   my $show_gridlines = $self->_get_number('horizontal_gridlines');
910   my $tic_distance = int(($graph_box->[3] - $graph_box->[1]) / ($tic_count - 1));
911   for my $count (0 .. $tic_count - 1) {
912     my $x1 = $graph_box->[0] - 5;
913     my $x2 = $graph_box->[0] + 5;
914     my $y1 = $graph_box->[3] - ($count * $tic_distance);
915
916     my $value = ($count*$interval)+$min;
917     if ($interval < 1 || ($value != int($value))) {
918         $value = sprintf("%.2f", $value);
919     }
920
921     my @box = $self->_text_bbox($value, 'legend')
922       or return;
923
924     $img->line(x1 => $x1, x2 => $x2, y1 => $y1, y2 => $y1, aa => 1, color => '000000');
925
926     my $width = $box[2];
927     my $height = $box[3];
928
929     $img->string(%text_info,
930                  x    => ($x1 - $width - 3),
931                  y    => ($y1 + ($height / 2)),
932                  text => $value
933                 );
934
935     if ($show_gridlines) {
936       # XXX - line styles!
937       for (my $i = $graph_box->[0]; $i < $graph_box->[2]; $i += 6) {
938         my $x1 = $i;
939         my $x2 = $i + 2;
940         if ($x2 > $graph_box->[2]) { $x2 = $graph_box->[2]; }
941         $img->line(x1 => $x1, x2 => $x2, y1 => $y1, y2 => $y1, aa => 1, color => '000000');
942       }
943     }
944   }
945
946 }
947
948 sub _draw_x_tics {
949   my $self = shift;
950
951   my $img = $self->_get_image();
952   my $graph_box = $self->_get_graph_box();
953   my $image_box = $self->_get_image_box();
954
955   my $labels = $self->_get_labels();
956
957   my $tic_count = (scalar @$labels) - 1;
958
959   my $has_columns = (defined $self->_get_data_series()->{'column'} || defined $self->_get_data_series()->{'stacked_column'});
960
961   # If we have columns, we want the x-ticks to show up in the middle of the column, not on the left edge
962   my $denominator = $tic_count;
963   if ($has_columns) {
964     $denominator ++;
965   }
966   my $tic_distance = ($graph_box->[2] - $graph_box->[0]) / ($denominator);
967   my %text_info = $self->_text_style('legend')
968     or return;
969
970   for my $count (0 .. $tic_count) {
971     my $label = $labels->[$count];
972     my $x1 = $graph_box->[0] + ($tic_distance * $count);
973
974     if ($has_columns) {
975       $x1 += $tic_distance / 2;
976     }
977     my $y1 = $graph_box->[3] + 5;
978     my $y2 = $graph_box->[3] - 5;
979
980     $img->line(x1 => $x1, x2 => $x1, y1 => $y1, y2 => $y2, aa => 1, color => '000000');
981
982     my @box = $self->_text_bbox($label, 'legend')
983       or return;
984
985     my $width = $box[2];
986     my $height = $box[3];
987
988     $img->string(%text_info,
989                  x    => ($x1 - ($width / 2)),
990                  y    => ($y1 + ($height + 5)),
991                  text => $label
992                 );
993
994   }
995 }
996
997 sub _valid_input {
998   my $self = shift;
999
1000   if (!defined $self->_get_data_series() || !keys %{$self->_get_data_series()}) {
1001     return $self->_error("No data supplied");
1002   }
1003
1004   my $data = $self->_get_data_series();
1005   if (defined $data->{'line'} && !scalar @{$data->{'line'}->[0]->{'data'}}) {
1006     return $self->_error("No values in data series");
1007   }
1008   if (defined $data->{'column'} && !scalar @{$data->{'column'}->[0]->{'data'}}) {
1009     return $self->_error("No values in data series");
1010   }
1011   if (defined $data->{'stacked_column'} && !scalar @{$data->{'stacked_column'}->[0]->{'data'}}) {
1012     return $self->_error("No values in data series");
1013   }
1014
1015   return 1;
1016 }
1017
1018 sub _set_column_count   { $_[0]->{'column_count'} = $_[1]; }
1019 sub _set_min_value      { $_[0]->{'min_value'} = $_[1]; }
1020 sub _set_max_value      { $_[0]->{'max_value'} = $_[1]; }
1021 sub _set_image_box      { $_[0]->{'image_box'} = $_[1]; }
1022 sub _set_graph_box      { $_[0]->{'graph_box'} = $_[1]; }
1023 sub _set_series_counter { $_[0]->{'series_counter'} = $_[1]; }
1024 sub _get_column_count   { return $_[0]->{'column_count'} }
1025 sub _get_min_value      { return $_[0]->{'min_value'} }
1026 sub _get_max_value      { return $_[0]->{'max_value'} }
1027 sub _get_image_box      { return $_[0]->{'image_box'} }
1028 sub _get_graph_box      { return $_[0]->{'graph_box'} }
1029 sub _get_series_counter { return $_[0]->{'series_counter'} }
1030
1031 1;