47038f31e8a20e1a818700d4784dc48ad9d326a7
[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
16 =over 4
17
18 =item add_data_series(\@data, $series_name)
19
20 Add a data series to the graph, of the default type.
21
22 =cut
23
24 sub add_data_series {
25   my $self = shift;
26   my $data_ref = shift;
27   my $series_name = shift;
28
29   my $series_type = $self->_get_default_series_type();
30   $self->_add_data_series($series_type, $data_ref, $series_name);
31
32   return;
33 }
34
35 =item add_column_data_series(\@data, $series_name)
36
37 Add a column data series to the graph.
38
39 =cut
40
41 sub add_column_data_series {
42   my $self = shift;
43   my $data_ref = shift;
44   my $series_name = shift;
45
46   $self->_add_data_series('column', $data_ref, $series_name);
47
48   return;
49 }
50
51 =item add_stacked_column_data_series(\@data, $series_name)
52
53 Add a stacked column data series to the graph.
54
55 =cut
56
57 sub add_stacked_column_data_series {
58   my $self = shift;
59   my $data_ref = shift;
60   my $series_name = shift;
61
62   $self->_add_data_series('stacked_column', $data_ref, $series_name);
63
64   return;
65 }
66
67 =item add_line_data_series(\@data, $series_name)
68
69 Add a line data series to the graph.
70
71 =cut
72
73 sub add_line_data_series {
74   my $self = shift;
75   my $data_ref = shift;
76   my $series_name = shift;
77
78   $self->_add_data_series('line', $data_ref, $series_name);
79
80   return;
81 }
82
83 =item add_area_data_series(\@data, $series_name)
84
85 Add a area data series to the graph.
86
87 =cut
88
89 sub add_area_data_series {
90   my $self = shift;
91   my $data_ref = shift;
92   my $series_name = shift;
93
94   $self->_add_data_series('area', $data_ref, $series_name);
95
96   return;
97 }
98
99
100 =item set_y_max($value)
101
102 Sets the maximum y value to be displayed.  This will be ignored if the y_max is lower than the highest value.
103
104 =cut
105
106 sub set_y_max {
107   $_[0]->{'custom_style'}->{'y_max'} = $_[1];
108 }
109
110 =item set_y_min($value)
111
112 Sets the minimum y value to be displayed.  This will be ignored if the y_min is higher than the lowest value.
113
114 =cut
115
116 sub set_y_min {
117   $_[0]->{'custom_style'}->{'y_min'} = $_[1];
118 }
119
120 =item set_column_padding($int)
121
122 Sets the padding between columns.  This is a percentage of the column width.  Defaults to 0.
123
124 =cut
125
126 sub set_column_padding {
127   $_[0]->{'custom_style'}->{'column_padding'} = $_[1];
128 }
129
130 =item set_range_padding($percentage)
131
132 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.
133
134 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.
135
136 =cut
137
138 sub set_range_padding {
139   $_[0]->{'custom_style'}->{'range_padding'} = $_[1];
140 }
141
142 =item set_negative_background($color)
143
144 Sets the background color used below the x axis.
145
146 =cut
147
148 sub set_negative_background {
149   $_[0]->{'custom_style'}->{'negative_bg'} = $_[1];
150 }
151
152 =item draw()
153
154 Draw the graph
155
156 =cut
157
158 sub draw {
159   my ($self, %opts) = @_;
160
161   if (!$self->_valid_input()) {
162     return;
163   }
164
165   $self->_style_setup(\%opts);
166
167   my $style = $self->{_style};
168
169   my $img = $self->_get_image()
170     or return;
171
172   my @image_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
173   $self->_set_image_box(\@image_box);
174
175   my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
176   $self->_draw_legend(\@chart_box);
177   if ($style->{title}{text}) {
178     $self->_draw_title($img, \@chart_box)
179       or return;
180   }
181
182   # Scale the graph box down to the widest graph that can cleanly hold the # of columns.
183   return unless $self->_get_data_range();
184   $self->_remove_tics_from_chart_box(\@chart_box);
185   my $column_count = $self->_get_column_count();
186
187   my $width = $self->_get_number('width');
188   my $height = $self->_get_number('height');
189
190   my $graph_width = $chart_box[2] - $chart_box[0];
191   my $graph_height = $chart_box[3] - $chart_box[1];
192
193   my $col_width = ($graph_width - 1) / $column_count;
194   if ($col_width > 1) {
195     $graph_width = int($col_width) * $column_count + 1;
196   }
197   else {
198     $graph_width = $col_width * $column_count + 1;
199   }
200
201   my $tic_count = $self->_get_y_tics();
202   my $tic_distance = ($graph_height-1) / ($tic_count - 1);
203   $graph_height = int($tic_distance * ($tic_count - 1));
204
205   my $bottom = $chart_box[1];
206   my $left   = $chart_box[0];
207
208   $self->{'_style'}{'graph_width'} = $graph_width;
209   $self->{'_style'}{'graph_height'} = $graph_height;
210
211   my @graph_box = ($left, $bottom, $left + $graph_width, $bottom + $graph_height);
212   $self->_set_graph_box(\@graph_box);
213
214   $img->box(
215             color   => $self->_get_color('outline.line'),
216             xmin    => $left,
217             xmax    => $left+$graph_width,
218             ymin    => $bottom,
219             ymax    => $bottom+$graph_height,
220             );
221
222   $img->box(
223             color   => $self->_get_color('bg'),
224             xmin    => $left + 1,
225             xmax    => $left+$graph_width - 1,
226             ymin    => $bottom + 1,
227             ymax    => $bottom+$graph_height-1 ,
228             filled  => 1,
229             );
230
231   my $min_value = $self->_get_min_value();
232   my $max_value = $self->_get_max_value();
233   my $value_range = $max_value - $min_value;
234
235   my $zero_position;
236   if ($value_range) {
237     $zero_position =  $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height-1);
238   }
239
240   if ($min_value < 0) {
241     $img->box(
242             color   => $self->_get_color('negative_bg'),
243             xmin    => $left + 1,
244             xmax    => $left+$graph_width- 1,
245             ymin    => $zero_position,
246             ymax    => $bottom+$graph_height - 1,
247             filled  => 1,
248     );
249     $img->line(
250             x1 => $left+1,
251             y1 => $zero_position,
252             x2 => $left + $graph_width,
253             y2 => $zero_position,
254             color => $self->_get_color('outline.line'),
255     );
256   }
257
258   if ($self->_get_data_series()->{'stacked_column'}) {
259     return unless $self->_draw_stacked_columns();
260   }
261   if ($self->_get_data_series()->{'column'}) {
262     return unless $self->_draw_columns();
263   }
264   if ($self->_get_data_series()->{'line'}) {
265     return unless $self->_draw_lines();
266   }
267   if ($self->_get_data_series()->{'area'}) {
268     return unless $self->_draw_area();
269   }
270
271   if ($self->_get_y_tics()) {
272     $self->_draw_y_tics();
273   }
274   if ($self->_get_labels()) {
275     $self->_draw_x_tics();
276   }
277
278   return $self->_get_image();
279 }
280
281 sub _get_data_range {
282   my $self = shift;
283
284   my $max_value = 0;
285   my $min_value = 0;
286   my $column_count = 0;
287
288   my ($sc_min, $sc_max, $sc_cols) = $self->_get_stacked_column_range();
289   my ($c_min, $c_max, $c_cols) = $self->_get_column_range();
290   my ($l_min, $l_max, $l_cols) = $self->_get_line_range();
291   my ($a_min, $a_max, $a_cols) = $self->_get_area_range();
292
293   # These are side by side...
294   $sc_cols += $c_cols;
295
296   $min_value = $self->_min(STARTING_MIN_VALUE, $sc_min, $c_min, $l_min, $a_min);
297   $max_value = $self->_max(0, $sc_max, $c_max, $l_max, $a_max);
298
299   my $config_min = $self->_get_number('y_min');
300   my $config_max = $self->_get_number('y_max');
301
302   if (defined $config_max && $config_max < $max_value) {
303     $config_max = undef;
304   }
305   if (defined $config_min && $config_min > $min_value) {
306     $config_min = undef;
307   }
308
309   my $range_padding = $self->_get_number('range_padding');
310   if (defined $config_min) {
311     $min_value = $config_min;
312   }
313   else {
314     if ($min_value > 0) {
315       $min_value = 0;
316     }
317     if ($range_padding && $min_value < 0) {
318       my $difference = $min_value * $range_padding / 100;
319       if ($min_value < -1 && $difference > -1) {
320         $difference = -1;
321       }
322       $min_value += $difference;
323     }
324   }
325   if (defined $config_max) {
326     $max_value = $config_max;
327   }
328   else {
329     if ($range_padding && $max_value > 0) {
330       my $difference = $max_value * $range_padding / 100;
331       if ($max_value > 1 && $difference < 1) {
332         $difference = 1;
333       }
334       $max_value += $difference;
335     }
336   }
337   $column_count = $self->_max(0, $sc_cols, $l_cols, $a_cols);
338
339   if ($self->_get_number('automatic_axis')) {
340     # In case this was set via a style, and not by the api method
341     eval { require Chart::Math::Axis; };
342     if ($@) {
343       return $self->_error("Can't use automatic_axis - $@");
344     }
345
346     my $axis = Chart::Math::Axis->new();
347     $axis->include_zero();
348     $axis->add_data($min_value, $max_value);
349     $max_value = $axis->top;
350     $min_value = $axis->bottom;
351     my $ticks     = $axis->ticks;
352     # The +1 is there because we have the bottom tick as well
353     $self->set_y_tics($ticks+1);
354   }
355
356   $self->_set_max_value($max_value);
357   $self->_set_min_value($min_value);
358   $self->_set_column_count($column_count);
359
360   return 1;
361 }
362
363 sub _min {
364   my $self = shift;
365   my $min = shift;
366
367   foreach my $value (@_) {
368     next unless defined $value;
369     if ($value < $min) { $min = $value; }
370   }
371   return $min;
372 }
373
374 sub _max {
375   my $self = shift;
376   my $min = shift;
377
378   foreach my $value (@_) {
379     next unless defined $value;
380     if ($value > $min) { $min = $value; }
381   }
382   return $min;
383 }
384
385 sub _get_line_range {
386   my $self = shift;
387   my $series = $self->_get_data_series()->{'line'};
388   return (undef, undef, 0) unless $series;
389
390   my $max_value = 0;
391   my $min_value = STARTING_MIN_VALUE;
392   my $column_count = 0;
393
394   my @series = @{$series};
395   foreach my $series (@series) {
396     my @data = @{$series->{'data'}};
397
398     if (scalar @data > $column_count) {
399       $column_count = scalar @data;
400     }
401
402     foreach my $value (@data) {
403       if ($value > $max_value) { $max_value = $value; }
404       if ($value < $min_value) { $min_value = $value; }
405     }
406   }
407
408   return ($min_value, $max_value, $column_count);
409 }
410
411 sub _get_area_range {
412   my $self = shift;
413   my $series = $self->_get_data_series()->{'area'};
414   return (undef, undef, 0) unless $series;
415
416   my $max_value = 0;
417   my $min_value = STARTING_MIN_VALUE;
418   my $column_count = 0;
419
420   my @series = @{$series};
421   foreach my $series (@series) {
422     my @data = @{$series->{'data'}};
423
424     if (scalar @data > $column_count) {
425       $column_count = scalar @data;
426     }
427
428     foreach my $value (@data) {
429       if ($value > $max_value) { $max_value = $value; }
430       if ($value < $min_value) { $min_value = $value; }
431     }
432   }
433
434   return ($min_value, $max_value, $column_count);
435 }
436
437
438 sub _get_column_range {
439   my $self = shift;
440
441   my $series = $self->_get_data_series()->{'column'};
442   return (undef, undef, 0) unless $series;
443
444   my $max_value = 0;
445   my $min_value = STARTING_MIN_VALUE;
446   my $column_count = 0;
447
448   my @series = @{$series};
449   foreach my $series (@series) {
450     my @data = @{$series->{'data'}};
451
452     foreach my $value (@data) {
453       $column_count++;
454       if ($value > $max_value) { $max_value = $value; }
455       if ($value < $min_value) { $min_value = $value; }
456     }
457   }
458
459   return ($min_value, $max_value, $column_count);
460 }
461
462 sub _get_stacked_column_range {
463   my $self = shift;
464
465   my $max_value = 0;
466   my $min_value = STARTING_MIN_VALUE;
467   my $column_count = 0;
468
469   return (undef, undef, 0) unless $self->_get_data_series()->{'stacked_column'};
470   my @series = @{$self->_get_data_series()->{'stacked_column'}};
471
472   my @max_entries;
473   my @min_entries;
474   for (my $i = scalar @series - 1; $i >= 0; $i--) {
475     my $series = $series[$i];
476     my $data = $series->{'data'};
477
478     for (my $i = 0; $i < scalar @$data; $i++) {
479       my $value = 0;
480       if ($data->[$i] > 0) {
481         $value = $data->[$i] + ($max_entries[$i] || 0);
482         $data->[$i] = $value;
483         $max_entries[$i] = $value;
484       }
485       elsif ($data->[$i] < 0) {
486         $value = $data->[$i] + ($min_entries[$i] || 0);
487         $data->[$i] = $value;
488         $min_entries[$i] = $value;
489       }
490       if ($value > $max_value) { $max_value = $value; }
491       if ($value < $min_value) { $min_value = $value; }
492     }
493     if (scalar @$data > $column_count) {
494       $column_count = scalar @$data;
495     }
496   }
497
498   return ($min_value, $max_value, $column_count);
499 }
500
501 sub _draw_legend {
502   my $self = shift;
503   my $chart_box = shift;
504   my $style = $self->{'_style'};
505
506   my @labels;
507   my $img = $self->_get_image();
508   if (my $series = $self->_get_data_series()->{'stacked_column'}) {
509     push @labels, map { $_->{'series_name'} } @$series;
510   }
511   if (my $series = $self->_get_data_series()->{'column'}) {
512     push @labels, map { $_->{'series_name'} } @$series;
513   }
514   if (my $series = $self->_get_data_series()->{'line'}) {
515     push @labels, map { $_->{'series_name'} } @$series;
516   }
517   if (my $series = $self->_get_data_series()->{'area'}) {
518     push @labels, map { $_->{'series_name'} } @$series;
519   }
520
521   if ($style->{features}{legend} && (scalar @labels)) {
522     $self->SUPER::_draw_legend($self->_get_image(), \@labels, $chart_box)
523       or return;
524   }
525   return;
526 }
527
528 sub _draw_flat_legend {
529   return 1;
530 }
531
532 sub _draw_lines {
533   my $self = shift;
534   my $style = $self->{'_style'};
535
536   my $img = $self->_get_image();
537
538   my $max_value = $self->_get_max_value();
539   my $min_value = $self->_get_min_value();
540   my $column_count = $self->_get_column_count();
541
542   my $value_range = $max_value - $min_value;
543
544   my $width = $self->_get_number('width');
545   my $height = $self->_get_number('height');
546
547   my $graph_width = $self->_get_number('graph_width');
548   my $graph_height = $self->_get_number('graph_height');
549
550   my $line_series = $self->_get_data_series()->{'line'};
551   my $series_counter = $self->_get_series_counter() || 0;
552
553   my $has_columns = (defined $self->_get_data_series()->{'column'} || $self->_get_data_series->{'stacked_column'}) ? 1 : 0;
554
555   my $col_width = int($graph_width / $column_count) -1;
556
557   my $graph_box = $self->_get_graph_box();
558   my $left = $graph_box->[0] + 1;
559   my $bottom = $graph_box->[1];
560
561   my $zero_position =  $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height - 1);
562
563   my $line_aa = $self->_get_number("lineaa");
564   foreach my $series (@$line_series) {
565     my @data = @{$series->{'data'}};
566     my $data_size = scalar @data;
567
568     my $interval;
569     if ($has_columns) {
570       $interval = $graph_width / ($data_size);
571     }
572     else {
573       $interval = $graph_width / ($data_size - 1);
574     }
575     my $color = $self->_data_color($series_counter);
576
577     # We need to add these last, otherwise the next line segment will overwrite half of the marker
578     my @marker_positions;
579     for (my $i = 0; $i < $data_size - 1; $i++) {
580       my $x1 = $left + $i * $interval;
581       my $x2 = $left + ($i + 1) * $interval;
582
583       $x1 += $has_columns * $interval / 2;
584       $x2 += $has_columns * $interval / 2;
585
586       my $y1 = $bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height;
587       my $y2 = $bottom + ($value_range - $data[$i + 1] + $min_value)/$value_range * $graph_height;
588
589       push @marker_positions, [$x1, $y1];
590       $img->line(x1 => $x1, y1 => $y1, x2 => $x2, y2 => $y2, aa => $line_aa, color => $color) || die $img->errstr;
591     }
592
593     my $x2 = $left + ($data_size - 1) * $interval;
594     $x2 += $has_columns * $interval / 2;
595
596     my $y2 = $bottom + ($value_range - $data[$data_size - 1] + $min_value)/$value_range * $graph_height;
597
598     push @marker_positions, [$x2, $y2];
599     foreach my $position (@marker_positions) {
600       $self->_draw_line_marker($position->[0], $position->[1], $series_counter);
601     }
602     $series_counter++;
603   }
604
605   $self->_set_series_counter($series_counter);
606   return 1;
607 }
608
609 sub _area_data_fill {
610   my ($self, $index, $box) = @_;
611
612   my %fill = $self->_data_fill($index, $box);
613
614   my $opacity = $self->_get_number("area.opacity");
615   $opacity == 1
616     and return %fill;
617
618   my $orig_fill = $fill{fill};
619   unless ($orig_fill) {
620     $orig_fill = Imager::Fill->new
621       (
622        solid => $fill{color},
623        combine => "normal",
624       );
625   }
626   return
627     (
628      fill => Imager::Fill->new
629      (
630       type => "opacity",
631       other => $orig_fill,
632       opacity => $opacity,
633      ),
634     );
635 }
636
637 sub _draw_area {
638   my $self = shift;
639   my $style = $self->{'_style'};
640
641   my $img = $self->_get_image();
642
643   my $max_value = $self->_get_max_value();
644   my $min_value = $self->_get_min_value();
645   my $column_count = $self->_get_column_count();
646
647   my $value_range = $max_value - $min_value;
648
649   my $width = $self->_get_number('width');
650   my $height = $self->_get_number('height');
651
652   my $graph_width = $self->_get_number('graph_width');
653   my $graph_height = $self->_get_number('graph_height');
654
655   my $area_series = $self->_get_data_series()->{'area'};
656   my $series_counter = $self->_get_series_counter() || 0;
657
658   my $col_width = int($graph_width / $column_count) -1;
659
660   my $graph_box = $self->_get_graph_box();
661   my $left = $graph_box->[0] + 1;
662   my $bottom = $graph_box->[1];
663   my $right = $graph_box->[2];
664   my $top = $graph_box->[3];
665
666   my $zero_position =  $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height - 1);
667
668   my $line_aa = $self->_get_number("lineaa");
669   foreach my $series (@$area_series) {
670     my @data = @{$series->{'data'}};
671     my $data_size = scalar @data;
672
673     my $interval = $graph_width / ($data_size - 1);
674
675     my $color = $self->_data_color($series_counter);
676
677     # We need to add these last, otherwise the next line segment will overwrite half of the marker
678     my @marker_positions;
679     my @polygon_points;
680     for (my $i = 0; $i < $data_size - 1; $i++) {
681       my $x1 = $left + $i * $interval;
682
683       my $y1 = $bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height;
684
685       if ($i == 0) {
686         push @polygon_points, [$x1, $top];
687       }
688       push @polygon_points, [$x1, $y1];
689
690       push @marker_positions, [$x1, $y1];
691     }
692
693     my $x2 = $left + ($data_size - 1) * $interval;
694
695     my $y2 = $bottom + ($value_range - $data[$data_size - 1] + $min_value)/$value_range * $graph_height;
696     push @polygon_points, [$x2, $y2];
697     push @polygon_points, [$x2, $top];
698     push @polygon_points, $polygon_points[0];
699
700     my @fill = $self->_area_data_fill($series_counter, [$left, $bottom, $right, $top]);
701     $img->polygon(points => [@polygon_points], @fill);
702
703     push @marker_positions, [$x2, $y2];
704     foreach my $position (@marker_positions) {
705       $self->_draw_line_marker($position->[0], $position->[1], $series_counter);
706     }
707     $series_counter++;
708   }
709
710   $self->_set_series_counter($series_counter);
711   return 1;
712 }
713
714
715
716 sub _line_marker {
717   my ($self, $index) = @_;
718
719   my $markers = $self->{'_style'}{'line_markers'};
720   if (!$markers) {
721     return;
722   }
723   my $marker = $markers->[$index % @$markers];
724
725   return $marker;
726 }
727
728 sub _draw_line_marker {
729   my $self = shift;
730   my ($x1, $y1, $series_counter) = @_;
731
732   my $img = $self->_get_image();
733
734   my $style = $self->_line_marker($series_counter);
735   return unless $style;
736
737   my $type = $style->{'shape'};
738   my $radius = $style->{'radius'};
739
740   my $line_aa = $self->_get_number("lineaa");
741   my $fill_aa = $self->_get_number("fill.aa");
742   if ($type eq 'circle') {
743     my @fill = $self->_data_fill($series_counter, [$x1 - $radius, $y1 - $radius, $x1 + $radius, $y1 + $radius]);
744     $img->circle(x => $x1, y => $y1, r => $radius, aa => $fill_aa, filled => 1, @fill);
745   }
746   elsif ($type eq 'square') {
747     my @fill = $self->_data_fill($series_counter, [$x1 - $radius, $y1 - $radius, $x1 + $radius, $y1 + $radius]);
748     $img->box(xmin => $x1 - $radius, ymin => $y1 - $radius, xmax => $x1 + $radius, ymax => $y1 + $radius, @fill);
749   }
750   elsif ($type eq 'diamond') {
751     # The gradient really doesn't work for diamond
752     my $color = $self->_data_color($series_counter);
753     $img->polygon(
754         points => [
755                     [$x1 - $radius, $y1],
756                     [$x1, $y1 + $radius],
757                     [$x1 + $radius, $y1],
758                     [$x1, $y1 - $radius],
759                   ],
760         filled => 1, color => $color, aa => $fill_aa);
761   }
762   elsif ($type eq 'triangle') {
763     # The gradient really doesn't work for triangle
764     my $color = $self->_data_color($series_counter);
765     $img->polygon(
766         points => [
767                     [$x1 - $radius, $y1 + $radius],
768                     [$x1 + $radius, $y1 + $radius],
769                     [$x1, $y1 - $radius],
770                   ],
771         filled => 1, color => $color, aa => $fill_aa);
772
773   }
774   elsif ($type eq 'x') {
775     my $color = $self->_data_color($series_counter);
776     $img->line(x1 => $x1 - $radius, y1 => $y1 -$radius, x2 => $x1 + $radius, y2 => $y1+$radius, aa => $line_aa, color => $color) || die $img->errstr;
777     $img->line(x1 => $x1 + $radius, y1 => $y1 -$radius, x2 => $x1 - $radius, y2 => $y1+$radius, aa => $line_aa, color => $color) || die $img->errstr;
778   }
779   elsif ($type eq 'plus') {
780     my $color = $self->_data_color($series_counter);
781     $img->line(x1 => $x1, y1 => $y1 -$radius, x2 => $x1, y2 => $y1+$radius, aa => $line_aa, color => $color) || die $img->errstr;
782     $img->line(x1 => $x1 + $radius, y1 => $y1, x2 => $x1 - $radius, y2 => $y1, aa => $line_aa, color => $color) || die $img->errstr;
783   }
784 }
785
786 sub _draw_columns {
787   my $self = shift;
788   my $style = $self->{'_style'};
789
790   my $img = $self->_get_image();
791
792   my $max_value = $self->_get_max_value();
793   my $min_value = $self->_get_min_value();
794   my $column_count = $self->_get_column_count();
795
796   my $value_range = $max_value - $min_value;
797
798   my $width = $self->_get_number('width');
799   my $height = $self->_get_number('height');
800
801   my $graph_width = $self->_get_number('graph_width');
802   my $graph_height = $self->_get_number('graph_height');
803
804
805   my $graph_box = $self->_get_graph_box();
806   my $left = $graph_box->[0] + 1;
807   my $bottom = $graph_box->[1];
808   my $zero_position =  int($bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height -1));
809
810   my $bar_width = $graph_width / $column_count;
811
812   my $outline_color;
813   if ($style->{'features'}{'outline'}) {
814     $outline_color = $self->_get_color('outline.line');
815   }
816
817   my $series_counter = $self->_get_series_counter() || 0;
818   my $col_series = $self->_get_data_series()->{'column'};
819   my $column_padding_percent = $self->_get_number('column_padding') || 0;
820   my $column_padding = int($column_padding_percent * $bar_width / 100);
821
822   # 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.
823   my $column_series = 0;
824
825   # If there are stacked columns, non-stacked columns need to start one to the right of where they would otherwise
826   my $has_stacked_columns = (defined $self->_get_data_series()->{'stacked_column'} ? 1 : 0);
827
828   for (my $series_pos = 0; $series_pos < scalar @$col_series; $series_pos++) {
829     my $series = $col_series->[$series_pos];
830     my @data = @{$series->{'data'}};
831     my $data_size = scalar @data;
832     for (my $i = 0; $i < $data_size; $i++) {
833       my $part1 = $bar_width * (scalar @$col_series * $i);
834       my $part2 = ($series_pos) * $bar_width;
835       my $x1 = $left + $part1 + $part2;
836       if ($has_stacked_columns) {
837         $x1 += ($bar_width * ($i+1));
838       }
839       $x1 = int($x1);
840
841       my $x2 = int($x1 + $bar_width - $column_padding)-1;
842       # Special case for when bar_width is less than 1.
843       if ($x2 < $x1) {
844         $x2 = $x1;
845       }
846
847       my $y1 = int($bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height);
848
849       my $color = $self->_data_color($series_counter);
850
851       if ($data[$i] > 0) {
852         my @fill = $self->_data_fill($series_counter, [$x1, $y1, $x2, $zero_position-1]);
853         $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position-1, @fill);
854         if ($style->{'features'}{'outline'}) {
855           $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position, color => $outline_color);
856         }
857       }
858       else {
859         my @fill = $self->_data_fill($series_counter, [$x1, $zero_position+1, $x2, $y1]);
860         $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1, @fill);
861         if ($style->{'features'}{'outline'}) {
862           $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1+1, color => $outline_color);
863         }
864       }
865     }
866
867     $series_counter++;
868     $column_series++;
869   }
870   $self->_set_series_counter($series_counter);
871   return 1;
872 }
873
874 sub _draw_stacked_columns {
875   my $self = shift;
876   my $style = $self->{'_style'};
877
878   my $img = $self->_get_image();
879
880   my $max_value = $self->_get_max_value();
881   my $min_value = $self->_get_min_value();
882   my $column_count = $self->_get_column_count();
883   my $value_range = $max_value - $min_value;
884
885   my $graph_box = $self->_get_graph_box();
886   my $left = $graph_box->[0] + 1;
887   my $bottom = $graph_box->[1];
888
889   my $graph_width = $self->_get_number('graph_width');
890   my $graph_height = $self->_get_number('graph_height');
891
892   my $bar_width = $graph_width / $column_count;
893   my $column_series = 0;
894   if (my $column_series_data = $self->_get_data_series()->{'column'}) {
895     $column_series = (scalar @$column_series_data);
896   }
897   $column_series++;
898
899   my $column_padding_percent = $self->_get_number('column_padding') || 0;
900   if ($column_padding_percent < 0) {
901     return $self->_error("Column padding less than 0");
902   }
903   if ($column_padding_percent > 100) {
904     return $self->_error("Column padding greater than 0");
905   }
906   my $column_padding = int($column_padding_percent * $bar_width / 100);
907
908   my $outline_color;
909   if ($style->{'features'}{'outline'}) {
910     $outline_color = $self->_get_color('outline.line');
911   }
912
913   my $zero_position =  $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height -1);
914   my $col_series = $self->_get_data_series()->{'stacked_column'};
915   my $series_counter = $self->_get_series_counter() || 0;
916
917   foreach my $series (@$col_series) {
918     my @data = @{$series->{'data'}};
919     my $data_size = scalar @data;
920     for (my $i = 0; $i < $data_size; $i++) {
921       my $part1 = $bar_width * $i * $column_series;
922       my $part2 = 0;
923       my $x1 = int($left + $part1 + $part2);
924       my $x2 = int($x1 + $bar_width - $column_padding) - 1;
925       # Special case for when bar_width is less than 1.
926       if ($x2 < $x1) {
927         $x2 = $x1;
928       }
929
930       my $y1 = int($bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height);
931
932       if ($data[$i] > 0) {
933         my @fill = $self->_data_fill($series_counter, [$x1, $y1, $x2, $zero_position-1]);
934         $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position-1, @fill);
935         if ($style->{'features'}{'outline'}) {
936           $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position, color => $outline_color);
937         }
938       }
939       else {
940         my @fill = $self->_data_fill($series_counter, [$x1, $zero_position+1, $x2, $y1]);
941         $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1, @fill);
942         if ($style->{'features'}{'outline'}) {
943           $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1+1, color => $outline_color);
944         }
945       }
946     }
947
948     $series_counter++;
949   }
950   $self->_set_series_counter($series_counter);
951   return 1;
952 }
953
954 sub _add_data_series {
955   my $self = shift;
956   my $series_type = shift;
957   my $data_ref = shift;
958   my $series_name = shift;
959
960   my $graph_data = $self->{'graph_data'} || {};
961
962   my $series = $graph_data->{$series_type} || [];
963
964   push @$series, { data => $data_ref, series_name => $series_name };
965
966   $graph_data->{$series_type} = $series;
967
968   $self->{'graph_data'} = $graph_data;
969   return;
970 }
971
972 =over
973
974 =item show_horizontal_gridlines()
975
976 Shows horizontal gridlines at the y-tics.
977
978 =cut
979
980 sub show_horizontal_gridlines {
981     $_[0]->{'custom_style'}->{'horizontal_gridlines'} = 1;
982 }
983
984 =item use_automatic_axis()
985
986 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.
987
988 =cut
989
990 sub use_automatic_axis {
991   eval { require Chart::Math::Axis; };
992   if ($@) {
993     return $_[0]->_error("use_automatic_axis - $@\nCalled from ".join(' ', caller)."\n");
994   }
995   $_[0]->{'custom_style'}->{'automatic_axis'} = 1;
996   return 1;
997 }
998
999
1000 =item set_y_tics($count)
1001
1002 Set the number of Y tics to use.  Their value and position will be determined by the data range.
1003
1004 =cut
1005
1006 sub set_y_tics {
1007   $_[0]->{'y_tics'} = $_[1];
1008 }
1009
1010 sub _get_y_tics {
1011   return $_[0]->{'y_tics'} || 0;
1012 }
1013
1014 sub _remove_tics_from_chart_box {
1015   my $self = shift;
1016   my $chart_box = shift;
1017
1018   # XXX - bad default
1019   my $tic_width = $self->_get_y_tic_width() || 10;
1020   my @y_tic_box = ($chart_box->[0], $chart_box->[1], $chart_box->[0] + $tic_width, $chart_box->[3]);
1021
1022   # XXX - bad default
1023   my $tic_height = $self->_get_x_tic_height() || 10;
1024   my @x_tic_box = ($chart_box->[0], $chart_box->[3] - $tic_height, $chart_box->[2], $chart_box->[3]);
1025
1026   $self->_remove_box($chart_box, \@y_tic_box);
1027   $self->_remove_box($chart_box, \@x_tic_box);
1028
1029   # If there's no title, the y-tics will be part off-screen.  Half of the x-tic height should be more than sufficient.
1030   my @y_tic_tops = ($chart_box->[0], $chart_box->[1], $chart_box->[2], $chart_box->[1] + int($tic_height / 2));
1031   $self->_remove_box($chart_box, \@y_tic_tops);
1032
1033   # Make sure that the first and last label fit
1034   if (my $labels = $self->_get_labels()) {
1035     if (my @box = $self->_text_bbox($labels->[0], 'legend')) {
1036       my @remove_box = ($chart_box->[0],
1037                         $chart_box->[1],
1038                         $chart_box->[0] + int($box[2] / 2) + 1,
1039                         $chart_box->[3]
1040                         );
1041
1042       $self->_remove_box($chart_box, \@remove_box);
1043     }
1044     if (my @box = $self->_text_bbox($labels->[-1], 'legend')) {
1045       my @remove_box = ($chart_box->[2] - int($box[2] / 2) - 1,
1046                         $chart_box->[1],
1047                         $chart_box->[2],
1048                         $chart_box->[3]
1049                         );
1050
1051       $self->_remove_box($chart_box, \@remove_box);
1052     }
1053   }
1054 }
1055
1056 sub _get_y_tic_width{
1057   my $self = shift;
1058   my $min = $self->_get_min_value();
1059   my $max = $self->_get_max_value();
1060   my $tic_count = $self->_get_y_tics();
1061
1062   my $interval = ($max - $min) / ($tic_count - 1);
1063
1064   my %text_info = $self->_text_style('legend')
1065     or return;
1066
1067   my $max_width = 0;
1068   for my $count (0 .. $tic_count - 1) {
1069     my $value = sprintf("%.2f", ($count*$interval)+$min);
1070
1071     my @box = $self->_text_bbox($value, 'legend');
1072     my $width = $box[2] - $box[0];
1073
1074     # For the tic width
1075     $width += 10;
1076     if ($width > $max_width) {
1077       $max_width = $width;
1078     }
1079   }
1080
1081   return $max_width;
1082 }
1083
1084 sub _get_x_tic_height {
1085   my $self = shift;
1086
1087   my $labels = $self->_get_labels();
1088
1089   if (!$labels) {
1090         return;
1091   }
1092
1093   my $tic_count = (scalar @$labels) - 1;
1094
1095   my %text_info = $self->_text_style('legend')
1096     or return;
1097
1098   my $max_height = 0;
1099   for my $count (0 .. $tic_count) {
1100     my $label = $labels->[$count];
1101
1102     my @box = $self->_text_bbox($label, 'legend');
1103
1104     my $height = $box[3] - $box[1];
1105
1106     # Padding + the tic
1107     $height += 10;
1108     if ($height > $max_height) {
1109       $max_height = $height;
1110     }
1111   }
1112
1113   return $max_height;
1114 }
1115
1116 sub _draw_y_tics {
1117   my $self = shift;
1118   my $min = $self->_get_min_value();
1119   my $max = $self->_get_max_value();
1120   my $tic_count = $self->_get_y_tics();
1121
1122   my $img = $self->_get_image();
1123   my $graph_box = $self->_get_graph_box();
1124   my $image_box = $self->_get_image_box();
1125
1126   my $interval = ($max - $min) / ($tic_count - 1);
1127
1128   my %text_info = $self->_text_style('legend')
1129     or return;
1130
1131   my $line_style = $self->_get_color('outline.line');
1132   my $show_gridlines = $self->_get_number('horizontal_gridlines');
1133   my $tic_distance = ($graph_box->[3] - $graph_box->[1]) / ($tic_count - 1);
1134   for my $count (0 .. $tic_count - 1) {
1135     my $x1 = $graph_box->[0] - 5;
1136     my $x2 = $graph_box->[0] + 5;
1137     my $y1 = int($graph_box->[3] - ($count * $tic_distance));
1138
1139     my $value = ($count*$interval)+$min;
1140     if ($interval < 1 || ($value != int($value))) {
1141         $value = sprintf("%.2f", $value);
1142     }
1143
1144     my @box = $self->_text_bbox($value, 'legend')
1145       or return;
1146
1147     $img->line(x1 => $x1, x2 => $x2, y1 => $y1, y2 => $y1, aa => 1, color => $line_style);
1148
1149     my $width = $box[2];
1150     my $height = $box[3];
1151
1152     $img->string(%text_info,
1153                  x    => ($x1 - $width - 3),
1154                  y    => ($y1 + ($height / 2)),
1155                  text => $value
1156                 );
1157
1158     if ($show_gridlines) {
1159       # XXX - line styles!
1160       for (my $i = $graph_box->[0]; $i < $graph_box->[2]; $i += 6) {
1161         my $x1 = $i;
1162         my $x2 = $i + 2;
1163         if ($x2 > $graph_box->[2]) { $x2 = $graph_box->[2]; }
1164         $img->line(x1 => $x1, x2 => $x2, y1 => $y1, y2 => $y1, aa => 1, color => $line_style);
1165       }
1166     }
1167   }
1168
1169 }
1170
1171 sub _draw_x_tics {
1172   my $self = shift;
1173
1174   my $img = $self->_get_image();
1175   my $graph_box = $self->_get_graph_box();
1176   my $image_box = $self->_get_image_box();
1177
1178   my $labels = $self->_get_labels();
1179
1180   my $tic_count = (scalar @$labels) - 1;
1181
1182   my $has_columns = (defined $self->_get_data_series()->{'column'} || defined $self->_get_data_series()->{'stacked_column'});
1183
1184   # If we have columns, we want the x-ticks to show up in the middle of the column, not on the left edge
1185   my $denominator = $tic_count;
1186   if ($has_columns) {
1187     $denominator ++;
1188   }
1189   my $tic_distance = ($graph_box->[2] - $graph_box->[0]) / ($denominator);
1190   my %text_info = $self->_text_style('legend')
1191     or return;
1192
1193   # If automatic axis is turned on, let's be selective about what labels we draw.
1194   my $max_size = 0;
1195   my $tic_skip = 0;
1196   if ($self->_get_number('automatic_axis')) {
1197     foreach my $label (@$labels) {
1198       my @box = $self->_text_bbox($label, 'legend');
1199       if ($box[2] > $max_size) {
1200         $max_size = $box[2];
1201       }
1202     }
1203
1204     # Give the max_size some padding...
1205     $max_size *= 1.2;
1206
1207     $tic_skip = int($max_size / $tic_distance) + 1;
1208   }
1209
1210   my $line_style = $self->_get_color('outline.line');
1211
1212   for my $count (0 .. $tic_count) {
1213     next if ($count % ($tic_skip + 1));
1214     my $label = $labels->[$count];
1215     my $x1 = $graph_box->[0] + ($tic_distance * $count);
1216
1217     if ($has_columns) {
1218       $x1 += $tic_distance / 2;
1219     }
1220
1221     $x1 = int($x1);
1222
1223     my $y1 = $graph_box->[3] + 5;
1224     my $y2 = $graph_box->[3] - 5;
1225
1226     $img->line(x1 => $x1, x2 => $x1, y1 => $y1, y2 => $y2, aa => 1, color => $line_style);
1227
1228     my @box = $self->_text_bbox($label, 'legend')
1229       or return;
1230
1231     my $width = $box[2];
1232     my $height = $box[3];
1233
1234     $img->string(%text_info,
1235                  x    => ($x1 - ($width / 2)),
1236                  y    => ($y1 + ($height + 5)),
1237                  text => $label
1238                 );
1239
1240   }
1241 }
1242
1243 sub _valid_input {
1244   my $self = shift;
1245
1246   if (!defined $self->_get_data_series() || !keys %{$self->_get_data_series()}) {
1247     return $self->_error("No data supplied");
1248   }
1249
1250   my $data = $self->_get_data_series();
1251   if (defined $data->{'line'} && !scalar @{$data->{'line'}->[0]->{'data'}}) {
1252     return $self->_error("No values in data series");
1253   }
1254   if (defined $data->{'column'} && !scalar @{$data->{'column'}->[0]->{'data'}}) {
1255     return $self->_error("No values in data series");
1256   }
1257   if (defined $data->{'stacked_column'} && !scalar @{$data->{'stacked_column'}->[0]->{'data'}}) {
1258     return $self->_error("No values in data series");
1259   }
1260
1261   return 1;
1262 }
1263
1264 sub _set_column_count   { $_[0]->{'column_count'} = $_[1]; }
1265 sub _set_min_value      { $_[0]->{'min_value'} = $_[1]; }
1266 sub _set_max_value      { $_[0]->{'max_value'} = $_[1]; }
1267 sub _set_image_box      { $_[0]->{'image_box'} = $_[1]; }
1268 sub _set_graph_box      { $_[0]->{'graph_box'} = $_[1]; }
1269 sub _set_series_counter { $_[0]->{'series_counter'} = $_[1]; }
1270 sub _get_column_count   { return $_[0]->{'column_count'} }
1271 sub _get_min_value      { return $_[0]->{'min_value'} }
1272 sub _get_max_value      { return $_[0]->{'max_value'} }
1273 sub _get_image_box      { return $_[0]->{'image_box'} }
1274 sub _get_graph_box      { return $_[0]->{'graph_box'} }
1275 sub _get_series_counter { return $_[0]->{'series_counter'} }
1276
1277 sub _style_defs {
1278   my ($self) = @_;
1279
1280   my %work = %{$self->SUPER::_style_defs()};
1281   $work{area} =
1282     {
1283      opacity => 0.5,
1284     };
1285
1286   return \%work;
1287 }
1288
1289 1;