changes from Patrick + manifest update
[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 =over 4
15
16 =item add_data_series(\@data, $series_name)
17
18 Add a data series to the graph, of the default type.
19
20 =cut
21
22 sub add_data_series {
23   my $self = shift;
24   my $data_ref = shift;
25   my $series_name = shift;
26
27   my $series_type = $self->_get_default_series_type();
28   $self->_add_data_series($series_type, $data_ref, $series_name);
29
30   return;
31 }
32
33 =item add_column_data_series(\@data, $series_name)
34
35 Add a column data series to the graph.
36
37 =cut
38
39 sub add_column_data_series {
40   my $self = shift;
41   my $data_ref = shift;
42   my $series_name = shift;
43
44   $self->_add_data_series('column', $data_ref, $series_name);
45
46   return;
47 }
48
49 =item add_stacked_column_data_series(\@data, $series_name)
50
51 Add a stacked column data series to the graph.
52
53 =cut
54
55 sub add_stacked_column_data_series {
56   my $self = shift;
57   my $data_ref = shift;
58   my $series_name = shift;
59
60   $self->_add_data_series('stacked_column', $data_ref, $series_name);
61
62   return;
63 }
64
65 =item add_line_data_series(\@data, $series_name)
66
67 Add a line data series to the graph.
68
69 =cut
70
71 sub add_line_data_series {
72   my $self = shift;
73   my $data_ref = shift;
74   my $series_name = shift;
75
76   $self->_add_data_series('line', $data_ref, $series_name);
77
78   return;
79 }
80
81 =item set_range_padding($percentage)
82
83 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.
84
85 Defaults to 10.
86
87 =cut
88
89 sub set_range_padding {
90   $_[0]->{'custom_style'}->{'range_padding'} = $_[1];
91 }
92
93 =item draw()
94
95 Draw the graph
96
97 =cut
98
99 sub draw {
100   my ($self, %opts) = @_;
101
102   if (!$self->_valid_input()) {
103     return;
104   }
105
106   $self->_style_setup(\%opts);
107
108   my $style = $self->{_style};
109
110   my $img = $self->_get_image()
111     or return;
112
113   my @image_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
114   $self->_set_image_box(\@image_box);
115
116   # Scale the graph box down to the widest graph that can cleanly hold the # of columns.
117   $self->_get_data_range();
118   my $column_count = $self->_get_column_count();
119
120   my $width = $self->_get_number('width');
121   my $height = $self->_get_number('height');
122   my $size = $self->_get_number('size');
123
124   my $bottom = ($height - $size) / 2;
125   my $left   = ($width - $size) / 2;
126
127   my $col_width = int($size / $column_count) -1;
128   my $graph_width = $col_width * $column_count + 1;
129
130   my @graph_box = ( $left, $bottom, $left + $graph_width - 1, $bottom + $size - 1 );
131   $self->_set_graph_box(\@graph_box);
132
133   $self->_draw_legend();
134
135   $img->box(
136             color   => $self->_get_color('outline.line'),
137             xmin    => $left,
138             xmax    => $left+$graph_width,
139             ymin    => $bottom,
140             ymax    => $bottom+$size,
141             );
142
143   $img->box(
144             color   => $self->_get_color('bg'),
145             xmin    => $left + 1,
146             xmax    => $left+$graph_width - 1,
147             ymin    => $bottom + 1,
148             ymax    => $bottom+$size -1 ,
149             filled  => 1,
150             );
151
152   my $min_value = $self->_get_min_value();
153   my $max_value = $self->_get_max_value();
154   my $value_range = $max_value - $min_value;
155
156   my $zero_position;
157   if ($value_range) {
158     $zero_position =  $bottom + $size - (-1*$min_value / $value_range) * ($size -1);
159   }
160
161   if ($min_value < 0) {
162     $img->box(
163             color   => $self->_get_color('negative_bg'),
164             xmin    => $left + 1,
165             xmax    => $left+$graph_width- 1,
166             ymin    => $zero_position,
167             ymax    => $bottom+$size -1,
168             filled  => 1,
169     );
170     $img->line(
171             x1 => $left+1,
172             y1 => $zero_position,
173             x2 => $left + $graph_width,
174             y2 => $zero_position,
175             color => $self->_get_color('outline.line'),
176     );
177   }
178
179   if ($self->_get_data_series()->{'stacked_column'}) {
180     $self->_draw_stacked_columns();
181   }
182   if ($self->_get_data_series()->{'column'}) {
183     $self->_draw_columns();
184   }
185   if ($self->_get_data_series()->{'line'}) {
186     $self->_draw_lines();
187   }
188   return $self->_get_image();
189 }
190
191 sub _get_data_range {
192   my $self = shift;
193
194   my $max_value = 0;
195   my $min_value = 0;
196   my $column_count = 0;
197
198   my ($sc_min, $sc_max, $sc_cols) = $self->_get_stacked_column_range();
199   my ($c_min, $c_max, $c_cols) = $self->_get_column_range();
200   my ($l_min, $l_max, $l_cols) = $self->_get_line_range();
201
202   # These are side by side...
203   $sc_cols += $c_cols;
204
205   $min_value = $self->_min(0, $sc_min, $c_min, $l_min);
206   $max_value = $self->_max(0, $sc_max, $c_max, $l_max);
207
208   my $range_padding = $self->_get_number('range_padding');
209   if (!defined $range_padding) {
210     $range_padding = 10;
211   }
212   if ($range_padding && $min_value < 0) {
213     my $difference = $min_value * $range_padding / 100;
214     if ($min_value < -1 && $difference > -1) {
215       $difference = -1;
216     }
217     $min_value += $difference;
218   }
219   if ($range_padding && $max_value > 0) {
220     my $difference = $max_value * $range_padding / 100;
221     if ($max_value > 1 && $difference < 1) {
222       $difference = 1;
223     }
224     $max_value += $difference;
225   }
226
227   $column_count = $self->_max(0, $sc_cols, $l_cols);
228
229   $self->_set_max_value($max_value);
230   $self->_set_min_value($min_value);
231   $self->_set_column_count($column_count);
232 }
233
234 sub _min {
235   my $self = shift;
236   my $min = shift;
237
238   foreach my $value (@_) {
239     if ($value < $min) { $min = $value; }
240   }
241   return $min;
242 }
243
244 sub _max {
245   my $self = shift;
246   my $min = shift;
247
248   foreach my $value (@_) {
249     if ($value > $min) { $min = $value; }
250   }
251   return $min;
252 }
253
254 sub _get_line_range {
255   my $self = shift;
256   my $series = $self->_get_data_series()->{'line'};
257   return (0, 0, 0) unless $series;
258
259   my $max_value = 0;
260   my $min_value = 0;
261   my $column_count = 0;
262
263   my @series = @{$series};
264   foreach my $series (@series) {
265     my @data = @{$series->{'data'}};
266
267     if (scalar @data > $column_count) {
268       $column_count = scalar @data;
269     }
270
271     foreach my $value (@data) {
272       if ($value > $max_value) { $max_value = $value; }
273       if ($value < $min_value) { $min_value = $value; }
274     }
275   }
276
277   return ($min_value, $max_value, $column_count);
278 }
279
280 sub _get_column_range {
281   my $self = shift;
282
283   my $series = $self->_get_data_series()->{'column'};
284   return (0, 0, 0) unless $series;
285
286   my $max_value = 0;
287   my $min_value = 0;
288   my $column_count = 0;
289
290   my @series = @{$series};
291   foreach my $series (@series) {
292     my @data = @{$series->{'data'}};
293
294     foreach my $value (@data) {
295       $column_count++;
296       if ($value > $max_value) { $max_value = $value; }
297       if ($value < $min_value) { $min_value = $value; }
298     }
299   }
300
301   return ($min_value, $max_value, $column_count);
302 }
303
304 sub _get_stacked_column_range {
305   my $self = shift;
306
307   my $max_value = 0;
308   my $min_value = 0;
309   my $column_count = 0;
310
311   return (0, 0, 0) unless $self->_get_data_series()->{'stacked_column'};
312   my @series = @{$self->_get_data_series()->{'stacked_column'}};
313
314   my @max_entries;
315   my @min_entries;
316   for (my $i = scalar @series - 1; $i >= 0; $i--) {
317     my $series = $series[$i];
318     my $data = $series->{'data'};
319
320     for (my $i = 0; $i < scalar @$data; $i++) {
321       my $value = 0;
322       if ($data->[$i] > 0) {
323         $value = $data->[$i] + ($max_entries[$i] || 0);
324         $data->[$i] = $value;
325         $max_entries[$i] = $value;
326       }
327       elsif ($data->[$i] < 0) {
328         $value = $data->[$i] + ($min_entries[$i] || 0);
329         $data->[$i] = $value;
330         $min_entries[$i] = $value;
331       }
332       if ($value > $max_value) { $max_value = $value; }
333       if ($value < $min_value) { $min_value = $value; }
334     }
335     if (scalar @$data > $column_count) {
336       $column_count = scalar @$data;
337     }
338   }
339
340   return ($min_value, $max_value, $column_count);
341 }
342
343 sub _draw_legend {
344   my $self = shift;
345   my $style = $self->{'_style'};
346
347   my @labels;
348   my $img = $self->_get_image();
349   if (my $series = $self->_get_data_series()->{'stacked_column'}) {
350     push @labels, map { $_->{'series_name'} } @$series;
351   }
352   if (my $series = $self->_get_data_series()->{'column'}) {
353     push @labels, map { $_->{'series_name'} } @$series;
354   }
355   if (my $series = $self->_get_data_series()->{'line'}) {
356     push @labels, map { $_->{'series_name'} } @$series;
357   }
358
359   if ($style->{features}{legend} && (scalar @labels)) {
360     $self->SUPER::_draw_legend($self->_get_image(), \@labels, $self->_get_image_box())
361       or return;
362   }
363   return;
364 }
365
366 sub _draw_flat_legend {
367   return 1;
368 }
369
370 sub _draw_lines {
371   my $self = shift;
372   my $style = $self->{'_style'};
373
374   my $img = $self->_get_image();
375
376   my $max_value = $self->_get_max_value();
377   my $min_value = $self->_get_min_value();
378   my $column_count = $self->_get_column_count();
379
380   my $value_range = $max_value - $min_value;
381
382   my $width = $self->_get_number('width');
383   my $height = $self->_get_number('height');
384   my $size = $self->_get_number('size');
385
386   my $bottom = ($height - $size) / 2;
387   my $left   = ($width - $size) / 2;
388
389   my $zero_position =  $bottom + $size - (-1*$min_value / $value_range) * ($size -1);
390
391   if ($self->_get_y_tics()) {
392     $self->_draw_y_tics();
393   }
394   if ($self->_get_labels()) {
395     $self->_draw_x_tics();
396   }
397
398   my $line_series = $self->_get_data_series()->{'line'};
399   my $series_counter = $self->_get_series_counter() || 0;
400
401   my $has_columns = (defined $self->_get_data_series()->{'column'} || $self->_get_data_series->{'stacked_column'}) ? 1 : 0;
402
403   my $col_width = int($size / $column_count) -1;
404   my $graph_width = $col_width * $column_count + 1;
405
406   foreach my $series (@$line_series) {
407     my @data = @{$series->{'data'}};
408     my $data_size = scalar @data;
409
410     my $interval;
411     if ($has_columns) {
412       $interval = $graph_width / ($data_size);
413     }
414     else {
415       $interval = $graph_width / ($data_size - 1);
416     }
417     my @fill = $self->_data_fill($series_counter, $self->_get_graph_box());
418     my $color = $self->_data_color($series_counter);
419     for (my $i = 0; $i < $data_size - 1; $i++) {
420       my $x1 = $left + $i * $interval;
421       my $x2 = $left + ($i + 1) * $interval;
422
423       $x1 += $has_columns * $interval / 2;
424       $x2 += $has_columns * $interval / 2;
425
426       my $y1 = $bottom + ($value_range - $data[$i] + $min_value)/$value_range * $size;
427       my $y2 = $bottom + ($value_range - $data[$i + 1] + $min_value)/$value_range * $size;
428
429       $img->line(x1 => $x1, y1 => $y1, x2 => $x2, y2 => $y2, aa => 1, color => $color) || die $img->errstr;
430       $img->circle(x => $x1, y => $y1, r => 3, aa => 1, filled => 1, @fill);
431     }
432
433     my $x2 = $left + ($data_size - 1) * $interval;
434     $x2 += $has_columns * $interval / 2;
435
436     my $y2 = $bottom + ($value_range - $data[$data_size - 1] + $min_value)/$value_range * $size;
437
438     $img->circle(x => $x2, y => $y2, r => 3, aa => 1, filled => 1, @fill);
439     $series_counter++;
440   }
441
442   $self->_set_series_counter($series_counter);
443   return;
444 }
445
446 sub _draw_columns {
447   my $self = shift;
448   my $style = $self->{'_style'};
449
450   my $img = $self->_get_image();
451
452   my $max_value = $self->_get_max_value();
453   my $min_value = $self->_get_min_value();
454   my $column_count = $self->_get_column_count();
455
456   my $value_range = $max_value - $min_value;
457
458   my $width = $self->_get_number('width');
459   my $height = $self->_get_number('height');
460   my $size = $self->_get_number('size');
461
462   my $bottom = ($height - $size) / 2;
463   my $left   = ($width - $size) / 2 + 1;
464
465   my $zero_position =  int($bottom + $size - (-1*$min_value / $value_range) * ($size -1));
466
467   if ($self->_get_y_tics()) {
468     $self->_draw_y_tics();
469   }
470   if ($self->_get_labels()) {
471     $self->_draw_x_tics();
472   }
473
474   my $bar_width = int(($size)/ $column_count - 2);
475
476   my $outline_color;
477   if ($style->{'features'}{'outline'}) {
478     $outline_color = $self->_get_color('outline.line');
479   }
480
481   my $series_counter = $self->_get_series_counter() || 0;
482   my $col_series = $self->_get_data_series()->{'column'};
483
484   # 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.
485   my $column_series = 0;
486
487   # If there are stacked columns, non-stacked columns need to start one to the right of where they would otherwise
488   my $has_stacked_columns = (defined $self->_get_data_series()->{'stacked_column'} ? 1 : 0);
489
490   for (my $series_pos = 0; $series_pos < scalar @$col_series; $series_pos++) {
491     my $series = $col_series->[$series_pos];
492     my @data = @{$series->{'data'}};
493     my $data_size = scalar @data;
494     my $color = $self->_data_color($series_counter);
495     for (my $i = 0; $i < $data_size; $i++) {
496       my $x1 = int($left + $bar_width * (scalar @$col_series * $i + $series_pos)) + scalar @$col_series * $i + $series_pos;
497       if ($has_stacked_columns) {
498         $x1 += ($i + 1) * $bar_width + $i + 1;
499       }
500       my $x2 = $x1 + $bar_width;
501
502       my $y1 = int($bottom + ($value_range - $data[$i] + $min_value)/$value_range * $size);
503
504       my $color = $self->_data_color($series_counter);
505
506     #  my @fill = $self->_data_fill($series_counter, [$x1, $y1, $x2, $zero_position]);
507       if ($data[$i] > 0) {
508         $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position-1, color => $color, filled => 1);
509         if ($style->{'features'}{'outline'}) {
510           $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position, color => $outline_color);
511         }
512       }
513       else {
514         $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1, color => $color, filled => 1);
515         if ($style->{'features'}{'outline'}) {
516           $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1+1, color => $outline_color);
517         }
518       }
519     }
520
521     $series_counter++;
522     $column_series++;
523   }
524   $self->_set_series_counter($series_counter);
525   return;
526 }
527
528 sub _draw_stacked_columns {
529   my $self = shift;
530   my $style = $self->{'_style'};
531
532   my $img = $self->_get_image();
533
534   my $max_value = $self->_get_max_value();
535   my $min_value = $self->_get_min_value();
536   my $column_count = $self->_get_column_count();
537   my $value_range = $max_value - $min_value;
538
539   my $graph_box = $self->_get_graph_box();
540   my $left = $graph_box->[0] + 1;
541   my $bottom = $graph_box->[1];
542   my $size = $self->_get_number('size');
543
544   if ($self->_get_y_tics()) {
545     $self->_draw_y_tics();
546   }
547   if ($self->_get_labels()) {
548     $self->_draw_x_tics();
549   }
550
551   my $bar_width = int($size / $column_count -2);
552   my $column_series = 0;
553   if (my $column_series_data = $self->_get_data_series()->{'column'}) {
554     $column_series = (scalar @$column_series_data);
555   }
556   $column_series++;
557
558   my $outline_color;
559   if ($style->{'features'}{'outline'}) {
560     $outline_color = $self->_get_color('outline.line');
561   }
562
563   my $zero_position =  $bottom + $size - (-1*$min_value / $value_range) * ($size -1);
564   my $col_series = $self->_get_data_series()->{'stacked_column'};
565   my $series_counter = $self->_get_series_counter() || 0;
566   foreach my $series (@$col_series) {
567     my @data = @{$series->{'data'}};
568     my $data_size = scalar @data;
569     my $color = $self->_data_color($series_counter);
570     for (my $i = 0; $i < $data_size; $i++) {
571       my $x1 = int($left + $bar_width * ($column_series * $i)) + $column_series * $i;
572 #      my $x1 = $left + $i * $size / ($data_size);
573       my $x2 = $x1 + $bar_width;
574
575       my $y1 = $bottom + ($value_range - $data[$i] + $min_value)/$value_range * $size;
576
577       if ($data[$i] > 0) {
578         $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position-1, color => $color, filled => 1);
579         if ($style->{'features'}{'outline'}) {
580           $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position, color => $outline_color);
581         }
582       }
583       else {
584         $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1, color => $color, filled => 1);
585         if ($style->{'features'}{'outline'}) {
586           $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1+1, color => $outline_color);
587         }
588       }
589     }
590
591     $series_counter++;
592   }
593   $self->_set_series_counter($series_counter);
594   return;
595 }
596
597 sub _add_data_series {
598   my $self = shift;
599   my $series_type = shift;
600   my $data_ref = shift;
601   my $series_name = shift;
602
603   my $graph_data = $self->{'graph_data'} || {};
604
605   my $series = $graph_data->{$series_type} || [];
606
607   push @$series, { data => $data_ref, series_name => $series_name };
608
609   $graph_data->{$series_type} = $series;
610
611   $self->{'graph_data'} = $graph_data;
612   return;
613 }
614
615 =over
616
617 =item set_y_tics($count)
618
619 Set the number of Y tics to use.  Their value and position will be determined by the data range.
620
621 =cut
622
623 sub set_y_tics {
624   $_[0]->{'y_tics'} = $_[1];
625 }
626
627 sub _get_y_tics {
628   return $_[0]->{'y_tics'};
629 }
630
631 sub _draw_y_tics {
632   my $self = shift;
633   my $min = $self->_get_min_value();
634   my $max = $self->_get_max_value();
635   my $tic_count = $self->_get_y_tics();
636
637   my $img = $self->_get_image();
638   my $graph_box = $self->_get_graph_box();
639   my $image_box = $self->_get_image_box();
640
641   my $interval = ($max - $min) / ($tic_count - 1);
642
643   my %text_info = $self->_text_style('legend')
644     or return;
645
646   my $tic_distance = ($graph_box->[3] - $graph_box->[1]) / ($tic_count - 1);
647   for my $count (0 .. $tic_count - 1) {
648     my $x1 = $graph_box->[0] - 5;
649     my $x2 = $graph_box->[0] + 5;
650     my $y1 = $graph_box->[3] - ($count * $tic_distance);
651
652     my $value = sprintf("%.2f", ($count*$interval)+$min);
653     if ($value < 0) {
654         $y1++;
655     }
656     my @box = $self->_text_bbox($value, 'legend')
657       or return;
658
659     $img->line(x1 => $x1, x2 => $x2, y1 => $y1, y2 => $y1, aa => 1, color => '000000');
660
661     my $width = $box[2];
662     my $height = $box[3];
663
664     $img->string(%text_info,
665                  x    => ($x1 - $width - 3),
666                  y    => ($y1 + ($height / 2)),
667                  text => $value
668                 );
669   }
670
671 }
672
673 sub _draw_x_tics {
674   my $self = shift;
675
676   my $img = $self->_get_image();
677   my $graph_box = $self->_get_graph_box();
678   my $image_box = $self->_get_image_box();
679
680   my $labels = $self->_get_labels();
681
682   my $tic_count = (scalar @$labels) - 1;
683
684   my $has_columns = (defined $self->_get_data_series()->{'column'} || defined $self->_get_data_series()->{'stacked_column'});
685
686   # If we have columns, we want the x-ticks to show up in the middle of the column, not on the left edge
687   my $denominator = $tic_count;
688   if ($has_columns) {
689     $denominator ++;
690   }
691   my $tic_distance = ($graph_box->[2] - $graph_box->[0]) / ($denominator);
692   my %text_info = $self->_text_style('legend')
693     or return;
694
695   for my $count (0 .. $tic_count) {
696     my $label = $labels->[$count];
697     my $x1 = $graph_box->[0] + ($tic_distance * $count);
698
699     if ($has_columns) {
700       $x1 += $tic_distance / 2;
701     }
702     my $y1 = $graph_box->[3] + 5;
703     my $y2 = $graph_box->[3] - 5;
704
705     $img->line(x1 => $x1, x2 => $x1, y1 => $y1, y2 => $y2, aa => 1, color => '000000');
706
707     my @box = $self->_text_bbox($label, 'legend')
708       or return;
709
710     my $width = $box[2];
711     my $height = $box[3];
712
713     $img->string(%text_info,
714                  x    => ($x1 - ($width / 2)),
715                  y    => ($y1 + ($height + 5)),
716                  text => $label
717                 );
718
719   }
720 }
721
722 sub _valid_input {
723   my $self = shift;
724
725   if (!defined $self->_get_data_series() || !keys %{$self->_get_data_series()}) {
726     return $self->_error("No data supplied");
727   }
728
729   my $data = $self->_get_data_series();
730   if (defined $data->{'line'} && !scalar @{$data->{'line'}->[0]->{'data'}}) {
731     return $self->_error("No values in data series");
732   }
733   if (defined $data->{'column'} && !scalar @{$data->{'column'}->[0]->{'data'}}) {
734     return $self->_error("No values in data series");
735   }
736   if (defined $data->{'stacked_column'} && !scalar @{$data->{'stacked_column'}->[0]->{'data'}}) {
737     return $self->_error("No values in data series");
738   }
739
740   return 1;
741 }
742
743 sub _set_column_count   { $_[0]->{'column_count'} = $_[1]; }
744 sub _set_min_value      { $_[0]->{'min_value'} = $_[1]; }
745 sub _set_max_value      { $_[0]->{'max_value'} = $_[1]; }
746 sub _set_image_box      { $_[0]->{'image_box'} = $_[1]; }
747 sub _set_graph_box      { $_[0]->{'graph_box'} = $_[1]; }
748 sub _set_series_counter { $_[0]->{'series_counter'} = $_[1]; }
749 sub _get_column_count   { return $_[0]->{'column_count'} }
750 sub _get_min_value      { return $_[0]->{'min_value'} }
751 sub _get_max_value      { return $_[0]->{'max_value'} }
752 sub _get_image_box      { return $_[0]->{'image_box'} }
753 sub _get_graph_box      { return $_[0]->{'graph_box'} }
754 sub _get_series_counter { return $_[0]->{'series_counter'} }
755
756
757
758 1;