1package Imager::Graph::Horizontal;
2
3=head1 NAME
4
5  Imager::Graph::Horizontal - A super class for line/bar charts
6
7=cut
8
9use strict;
10use vars qw(@ISA);
11use Imager::Graph;
12@ISA = qw(Imager::Graph);
13
14use constant STARTING_MIN_VALUE => 99999;
15
16our $VERSION = "0.10";
17
18=over 4
19
20=item add_data_series(\@data, $series_name)
21
22Add a data series to the graph, of the default type.
23
24=cut
25
26sub add_data_series {
27  my $self = shift;
28  my $data_ref = shift;
29  my $series_name = shift;
30
31  my $series_type = $self->_get_default_series_type();
32  $self->_add_data_series($series_type, $data_ref, $series_name);
33
34  return;
35}
36
37=item add_bar_data_series(\@data, $series_name)
38
39Add a bar data series to the graph.
40
41=cut
42
43sub add_bar_data_series {
44  my $self = shift;
45  my $data_ref = shift;
46  my $series_name = shift;
47
48  $self->_add_data_series('bar', $data_ref, $series_name);
49
50  return;
51}
52
53=item add_line_data_series(\@data, $series_name)
54
55Add a line data series to the graph.
56
57=cut
58
59sub add_line_data_series {
60  my $self = shift;
61  my $data_ref = shift;
62  my $series_name = shift;
63
64  $self->_add_data_series('line', $data_ref, $series_name);
65
66  return;
67}
68
69=item set_column_padding($int)
70
71Sets the number of pixels that should go between columns of data.
72
73=cut
74
75sub set_column_padding {
76  $_[0]->{'custom_style'}->{'column_padding'} = $_[1];
77}
78
79=item set_negative_background($color)
80
81Sets the background color or fill used below the y axis.
82
83=cut
84
85sub set_negative_background {
86  $_[0]->{'custom_style'}->{'negative_bg'} = $_[1];
87}
88
89=item draw()
90
91Draw the graph
92
93=cut
94
95sub draw {
96  my ($self, %opts) = @_;
97
98  if (!$self->_valid_input()) {
99    return;
100  }
101
102  $self->_style_setup(\%opts);
103
104  my $style = $self->{_style};
105
106  $self->_make_img
107    or return;
108
109  my $img = $self->_get_image()
110    or return;
111
112  my @image_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
113  $self->_set_image_box(\@image_box);
114
115  my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
116  $self->_draw_legend(\@chart_box);
117  if ($style->{title}{text}) {
118    $self->_draw_title($img, \@chart_box)
119      or return;
120  }
121
122  # Scale the graph box down to the widest graph that can cleanly hold the # of columns.
123  return unless $self->_get_data_range();
124  $self->_remove_tics_from_chart_box(\@chart_box, \%opts);
125  my $column_count = $self->_get_column_count();
126
127  my $width = $self->_get_number('width');
128  my $height = $self->_get_number('height');
129
130  my $graph_width = $chart_box[2] - $chart_box[0];
131  my $graph_height = $chart_box[3] - $chart_box[1];
132
133  my $col_height = ($graph_height - 1) / $column_count;
134  if ($col_height > 1) {
135    $graph_height = int($col_height) * $column_count + 1;
136  }
137  else {
138    $graph_height = $col_height * $column_count + 1;
139  }
140
141  my $tic_count = $self->_get_x_tics();
142  my $tic_distance = int(($graph_width -1) / ($tic_count - 1));
143  $graph_width = $tic_distance * ($tic_count - 1);
144
145  my $top = $chart_box[1];
146  my $left   = $chart_box[0];
147
148  $self->{'_style'}{'graph_width'} = $graph_width;
149  $self->{'_style'}{'graph_height'} = $graph_height;
150
151  my @graph_box = ($left, $top, $left + $graph_width, $top + $graph_height);
152
153  $self->_set_graph_box(\@graph_box);
154
155  my @fill_box = @graph_box;
156
157  if ($self->_feature_enabled("graph_outline")) {
158    my @line = $self->_get_line("graph.outline")
159      or return;
160
161    $self->_box(
162		@line,
163		box => \@fill_box,
164		img => $img,
165	       );
166    ++$fill_box[0];
167    ++$fill_box[1];
168    --$fill_box[2];
169    --$fill_box[3];
170  }
171
172  {
173    my @back_fill = $self->_get_fill("graph.fill", \@fill_box)
174      or return;
175    $img->box(
176	      @back_fill,
177	      box => \@fill_box,
178	     );
179  }
180
181  my $min_value = $self->_get_min_value();
182  my $max_value = $self->_get_max_value();
183  my $value_range = $max_value - $min_value;
184
185  my $zero_position;
186  if ($value_range) {
187    $zero_position =  $left + (-1*$min_value / $value_range) * ($graph_width-1);
188  }
189
190  if ($min_value < 0) {
191    my @neg_box = ( $left+1, $top+1, $zero_position, $top+$graph_height - 1 );
192    my @neg_fill = $self->_get_fill('negative_bg', \@neg_box)
193      or return;
194
195    $img->box(
196	      @neg_fill,
197	      box => \@neg_box,
198    );
199    $img->line(
200            x1 => $zero_position,
201            y1 => $top,
202            x2 => $zero_position,
203            y2 => $top + $graph_height,
204            color => $self->_get_color('outline.line'),
205    );
206  }
207
208  $self->_reset_series_counter();
209
210  if ($self->_get_data_series()->{'bar'}) {
211    $self->_draw_bars();
212  }
213  if ($self->_get_data_series()->{'line'}) {
214    $self->_draw_lines();
215  }
216
217  if ($self->_get_x_tics()) {
218    $self->_draw_x_tics();
219  }
220  if ($self->_get_labels(\%opts)) {
221    $self->_draw_y_tics(\%opts);
222  }
223
224  return $self->_get_image();
225}
226
227sub _get_data_range {
228  my $self = shift;
229
230  my $max_value = 0;
231  my $min_value = 0;
232  my $column_count = 0;
233
234  my ($b_min, $b_max, $b_cols) = $self->_get_bar_range();
235  my ($l_min, $l_max, $l_cols) = $self->_get_line_range();
236
237  $min_value = $self->_min(STARTING_MIN_VALUE, $b_min, $l_min);
238  $max_value = $self->_max(0, $b_max, $l_max);
239  $column_count = $self->_max(0, $b_cols, $l_cols);
240
241  my $config_min = $self->_get_number('x_min');
242  my $config_max = $self->_get_number('x_max');
243
244  if (defined $config_max && $config_max < $max_value) {
245    $config_max = undef;
246  }
247  if (defined $config_min && $config_min > $min_value) {
248    $config_min = undef;
249  }
250
251  my $range_padding = $self->_get_number('range_padding');
252  if (defined $config_min) {
253    $min_value = $config_min;
254  }
255  else {
256    if ($min_value > 0) {
257      $min_value = 0;
258    }
259    if ($range_padding && $min_value < 0) {
260      my $difference = $min_value * $range_padding / 100;
261      if ($min_value < -1 && $difference > -1) {
262        $difference = -1;
263      }
264      $min_value += $difference;
265    }
266  }
267  if (defined $config_max) {
268    $max_value = $config_max;
269  }
270  else {
271    if ($range_padding && $max_value > 0) {
272      my $difference = $max_value * $range_padding / 100;
273      if ($max_value > 1 && $difference < 1) {
274        $difference = 1;
275      }
276      $max_value += $difference;
277    }
278  }
279
280  if ($self->_get_number('automatic_axis')) {
281    # In case this was set via a style, and not by the api method
282    eval { require Chart::Math::Axis; };
283    if ($@) {
284      return $self->_error("Can't use automatic_axis - $@");
285    }
286
287    my $axis = Chart::Math::Axis->new();
288    $axis->include_zero();
289    $axis->add_data($min_value, $max_value);
290    $max_value = $axis->top;
291    $min_value = $axis->bottom;
292    my $ticks     = $axis->ticks;
293    # The +1 is there because we have the bottom tick as well
294    $self->set_x_tics($ticks+1);
295  }
296
297  $self->_set_max_value($max_value);
298  $self->_set_min_value($min_value);
299  $self->_set_column_count($column_count);
300
301  return 1;
302}
303
304sub _min {
305  my $self = shift;
306  my $min = shift;
307
308  foreach my $value (@_) {
309    next unless defined $value;
310    if ($value < $min) { $min = $value; }
311  }
312  return $min;
313}
314
315sub _max {
316  my $self = shift;
317  my $min = shift;
318
319  foreach my $value (@_) {
320    next unless defined $value;
321    if ($value > $min) { $min = $value; }
322  }
323  return $min;
324}
325
326sub _get_line_range {
327  my $self = shift;
328  my $series = $self->_get_data_series()->{'line'};
329  return (undef, undef, 0) unless $series;
330
331  my $max_value = 0;
332  my $min_value = STARTING_MIN_VALUE;
333  my $column_count = 0;
334
335  my @series = @{$series};
336  foreach my $series (@series) {
337    my @data = @{$series->{'data'}};
338
339    if (scalar @data > $column_count) {
340      $column_count = scalar @data;
341    }
342
343    foreach my $value (@data) {
344      if ($value > $max_value) { $max_value = $value; }
345      if ($value < $min_value) { $min_value = $value; }
346    }
347  }
348
349  return ($min_value, $max_value, $column_count);
350}
351
352
353
354sub _get_bar_range {
355  my $self = shift;
356
357  my $series = $self->_get_data_series()->{'bar'};
358  return (undef, undef, 0) unless $series;
359
360  my $max_value = 0;
361  my $min_value = STARTING_MIN_VALUE;
362  my $column_count = 0;
363
364  my @series = @{$series};
365  foreach my $series (@series) {
366    my @data = @{$series->{'data'}};
367
368    foreach my $value (@data) {
369      $column_count++;
370      if ($value > $max_value) { $max_value = $value; }
371      if ($value < $min_value) { $min_value = $value; }
372    }
373  }
374
375  return ($min_value, $max_value, $column_count);
376}
377
378
379sub _draw_legend {
380  my $self = shift;
381  my $chart_box = shift;
382  my $style = $self->{'_style'};
383
384  my @labels;
385  my $img = $self->_get_image();
386  if (my $series = $self->_get_data_series()->{'bar'}) {
387    push @labels, map { $_->{'series_name'} } @$series;
388  }
389
390  if ($style->{features}{legend} && (scalar @labels)) {
391    $self->SUPER::_draw_legend($self->_get_image(), \@labels, $chart_box)
392      or return;
393  }
394  return;
395}
396
397sub _draw_flat_legend {
398  return 1;
399}
400
401sub _draw_lines {
402  my $self = shift;
403  my $style = $self->{'_style'};
404
405  my $img = $self->_get_image();
406
407  my $max_value = $self->_get_max_value();
408  my $min_value = $self->_get_min_value();
409  my $column_count = $self->_get_column_count();
410
411  my $value_range = $max_value - $min_value;
412
413  my $width = $self->_get_number('width');
414  my $height = $self->_get_number('height');
415
416  my $graph_width = $self->_get_number('graph_width');
417  my $graph_height = $self->_get_number('graph_height');
418
419  my $line_series = $self->_get_data_series()->{'line'};
420  my $series_counter = $self->_get_series_counter() || 0;
421
422  my $has_columns = (defined $self->_get_data_series()->{'column'} || $self->_get_data_series->{'stacked_column'}) ? 1 : 0;
423
424  my $col_height = int($graph_height / $column_count) -1;
425
426  my $graph_box = $self->_get_graph_box();
427  my $left = $graph_box->[0] + 1;
428  my $bottom = $graph_box->[1];
429
430  my $zero_position =  $left + $graph_width - (-1*$min_value / $value_range) * ($graph_width - 1);
431
432  my $line_aa = $self->_get_number("lineaa");
433  foreach my $series (@$line_series) {
434    my @data = @{$series->{'data'}};
435    my $data_size = scalar @data;
436
437    my $interval;
438    if ($has_columns) {
439      $interval = $graph_height / ($data_size);
440    }
441    else {
442      $interval = $graph_height / ($data_size - 1);
443    }
444    my $color = $self->_data_color($series_counter);
445
446    # We need to add these last, otherwise the next line segment will overwrite half of the marker
447    my @marker_positions;
448    for (my $i = 0; $i < $data_size - 1; $i++) {
449      my $y1 = $bottom + $i * $interval;
450      my $y2 = $bottom + ($i + 1) * $interval;
451
452      $y1 += $has_columns * $interval / 2;
453      $y2 += $has_columns * $interval / 2;
454
455      my $x1 = $left + ($value_range - $data[$i] + $min_value)/$value_range * $graph_width;
456      my $x2 = $left + ($value_range - $data[$i + 1] + $min_value)/$value_range * $graph_width;
457
458      push @marker_positions, [$x1, $y1];
459      $img->line(x1 => $x1, y1 => $y1, x2 => $x2, y2 => $y2, aa => $line_aa, color => $color) || die $img->errstr;
460    }
461
462
463    my $y2 = $bottom + ($data_size - 1) * $interval;
464    $y2 += $has_columns * $interval / 2;
465
466    my $x2 = $left + ($value_range - $data[$data_size - 1] + $min_value)/$value_range * $graph_width;
467
468    if ($self->_feature_enabled("linemarkers")) {
469      push @marker_positions, [$x2, $y2];
470      foreach my $position (@marker_positions) {
471	$self->_draw_line_marker($position->[0], $position->[1], $series_counter);
472      }
473    }
474    $series_counter++;
475  }
476
477  $self->_set_series_counter($series_counter);
478  return;
479}
480
481sub _draw_bars {
482  my $self = shift;
483  my $style = $self->{'_style'};
484
485  my $img = $self->_get_image();
486
487  my $max_value = $self->_get_max_value();
488  my $min_value = $self->_get_min_value();
489  my $column_count = $self->_get_column_count();
490
491  my $value_range = $max_value - $min_value;
492
493  my $width = $self->_get_number('width');
494  my $height = $self->_get_number('height');
495
496  my $graph_width = $self->_get_number('graph_width');
497  my $graph_height = $self->_get_number('graph_height');
498
499
500  my $graph_box = $self->_get_graph_box();
501  my $bottom = $graph_box->[1] + 1;
502  my $left  = $graph_box->[0];
503
504  my $zero_position =  int($left + (-1*$min_value / $value_range) * ($graph_width-1));
505
506  my $bar_height = $graph_height / $column_count;
507
508  my $outline_color;
509  if ($style->{'features'}{'outline'}) {
510    $outline_color = $self->_get_color('outline.line');
511  }
512
513  my $series_counter = $self->_get_series_counter() || 0;
514  my $col_series = $self->_get_data_series()->{'bar'};
515  my $column_padding = $self->_get_number('column_padding') || 0;
516
517  # 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.
518  my $column_series = 0;
519
520  for (my $series_pos = 0; $series_pos < scalar @$col_series; $series_pos++) {
521    my $series = $col_series->[$series_pos];
522    my @data = @{$series->{'data'}};
523    my $data_size = scalar @data;
524    for (my $i = 0; $i < $data_size; $i++) {
525
526      my $part1 = $bar_height * (scalar @$col_series * $i);
527      my $part2 = ($series_pos) * $bar_height;
528      my $y1 = int($bottom + $part1 + $part2);
529
530      my $y2 = int($y1 + $bar_height - $column_padding)-1;
531      # Special case for when bar_height is less than 1.
532      if ($y2 < $y1) {
533        $y2 = $y1;
534      }
535
536      my $x1 = int($left - ($min_value - $data[$i]) / $value_range * $graph_width);
537
538      my $color = $self->_data_color($series_counter);
539
540      if ($data[$i] > 0) {
541        my @fill = $self->_data_fill($series_counter, [$zero_position+1, $y1, $x1, $y2]);
542        $img->box(xmax => $x1, xmin => $zero_position+1, ymin => $y1, ymax => $y2, @fill);
543        if ($style->{'features'}{'outline'}) {
544          $img->box(xmax => $x1, xmin => $zero_position, ymin => $y1, ymax => $y2, color => $outline_color);
545        }
546      }
547      elsif ($data[$i] == 0) {
548      }
549      else {
550        my @fill = $self->_data_fill($series_counter, [$x1, $y1, $zero_position, $y2]);
551        $img->box(xmax  => $zero_position , xmin => $x1, ymin => $y1, ymax => $y2, @fill);
552        if ($style->{'features'}{'outline'}) {
553          $img->box(xmax => $zero_position, xmin => $x1, ymin => $y1, ymax => $y2, color => $outline_color);
554        }
555      }
556    }
557
558    $series_counter++;
559    $column_series++;
560  }
561  $self->_set_series_counter($series_counter);
562  return;
563}
564
565sub _add_data_series {
566  my $self = shift;
567  my $series_type = shift;
568  my $data_ref = shift;
569  my $series_name = shift;
570
571  my $graph_data = $self->{'graph_data'} || {};
572
573  my $series = $graph_data->{$series_type} || [];
574
575  push @$series, { data => $data_ref, series_name => $series_name };
576
577  $graph_data->{$series_type} = $series;
578
579  $self->{'graph_data'} = $graph_data;
580  return;
581}
582
583=over
584
585=item show_vertical_gridlines()
586
587Shows vertical gridlines at the y-tics.
588
589Feature: vertical_gridlines
590
591=cut
592
593sub show_vertical_gridlines {
594    $_[0]->{'custom_style'}{features}{'vertical_gridlines'} = 1;
595}
596
597=item set_vertical_gridline_style(color => ..., style => ...)
598
599Set the color and style of the lines drawn for gridlines.
600
601Style equivalent: vgrid
602
603=cut
604
605sub set_vertical_gridline_style {
606  my ($self, %opts) = @_;
607
608  $self->{custom_style}{vgrid} ||= {};
609  @{$self->{custom_style}{vgrid}}{keys %opts} = values %opts;
610
611  return 1;
612}
613
614=item show_line_markers()
615
616=item show_line_markers($value)
617
618Feature: linemarkers.
619
620If $value is missing or true, draw markers on a line data series.
621
622Note: line markers are drawn by default.
623
624=cut
625
626sub show_line_markers {
627  my ($self, $value) = @_;
628
629  @_ > 1 or $value = 1;
630
631  $self->{custom_style}{features}{linemarkers} = $value;
632
633  return 1;
634}
635
636=item use_automatic_axis()
637
638Automatically 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.
639
640=cut
641
642sub use_automatic_axis {
643  eval { require Chart::Math::Axis; };
644  if ($@) {
645    return $_[0]->_error("use_automatic_axis - $@\nCalled from ".join(' ', caller)."\n");
646  }
647  $_[0]->{'custom_style'}->{'automatic_axis'} = 1;
648  return 1;
649}
650
651
652=item set_x_tics($count)
653
654Set the number of X tics to use.  Their value and position will be determined by the data range.
655
656=cut
657
658sub set_x_tics {
659  $_[0]->{'x_tics'} = $_[1];
660}
661
662sub _get_x_tics {
663  return $_[0]->{'x_tics'} || 0;
664}
665
666sub _remove_tics_from_chart_box {
667  my ($self, $chart_box, $opts) = @_;
668
669  # XXX - bad default
670  my $tic_width = $self->_get_y_tic_width($opts) || 10;
671  my @y_tic_box = ($chart_box->[0], $chart_box->[1], $chart_box->[0] + $tic_width, $chart_box->[3]);
672
673  # XXX - bad default
674  my $tic_height = $self->_get_x_tic_height() || 10;
675  my @x_tic_box = ($chart_box->[0], $chart_box->[3] - $tic_height, $chart_box->[2], $chart_box->[3]);
676
677  $self->_remove_box($chart_box, \@y_tic_box);
678  $self->_remove_box($chart_box, \@x_tic_box);
679
680  # If there's no title, the y-tics will be part off-screen.  Half of the x-tic height should be more than sufficient.
681  my @y_tic_tops = ($chart_box->[0], $chart_box->[1], $chart_box->[2], $chart_box->[1] + int($tic_height / 2));
682  $self->_remove_box($chart_box, \@y_tic_tops);
683
684    if (my @box = $self->_text_bbox($self->_get_max_value(), 'legend')) {
685      my @remove_box = ($chart_box->[2] - int($box[2] / 2) - 1,
686                        $chart_box->[1],
687                        $chart_box->[2],
688                        $chart_box->[3]
689                        );
690
691      $self->_remove_box($chart_box, \@remove_box);
692    }
693
694
695}
696
697sub _get_y_tic_width {
698  my ($self, $opts) = @_;
699
700  my $labels = $self->_get_labels($opts);
701
702  if (!$labels) {
703    return;
704  }
705
706  my %text_info = $self->_text_style('legend')
707    or return;
708
709  my $max_width = 0;
710  foreach my $label (@$labels) {
711    my @box = $self->_text_bbox($label, 'legend');
712    my $width = $box[2] + 5;
713    # For the tic itself...
714    $width += 10;
715    if ($width > $max_width) {
716      $max_width = $width;
717    }
718  }
719  return $max_width;
720}
721
722sub _get_x_tic_height {
723  my $self = shift;
724
725  my $min = $self->_get_min_value();
726  my $max = $self->_get_max_value();
727  my $tic_count = $self->_get_x_tics();
728
729  my $interval = ($max - $min) / ($tic_count - 1);
730
731  my %text_info = $self->_text_style('legend')
732    or return;
733
734  my $max_height = 0;
735  for my $count (0 .. $tic_count - 1) {
736    my $value = sprintf("%.2f", ($count*$interval)+$min);
737
738    my @box = $self->_text_bbox($value, 'legend');
739    my $height = $box[3] - $box[1];
740
741    # For the tic width
742    $height += 10;
743    if ($height > $max_height) {
744      $max_height = $height;
745    }
746  }
747
748
749  return $max_height;
750}
751
752sub _draw_y_tics {
753  my ($self, $opts) = @_;
754
755  my $img = $self->_get_image();
756  my $graph_box = $self->_get_graph_box();
757  my $image_box = $self->_get_image_box();
758
759  my $labels = $self->_get_labels($opts);
760
761  my $tic_count = (scalar @$labels) - 1;
762
763  my $has_columns = defined $self->_get_data_series()->{'bar'};
764
765  # If we have columns, we want the x-ticks to show up in the middle of the column, not on the left edge
766  my $denominator = $tic_count;
767  if ($has_columns) {
768    $denominator ++;
769  }
770  my $tic_distance = ($graph_box->[3] - $graph_box->[1]) / ($denominator);
771  my %text_info = $self->_text_style('legend')
772    or return;
773
774  for my $count (0 .. $tic_count) {
775    my $label = $labels->[$count];
776
777    my $x1 = $graph_box->[0] - 5;
778    my $x2 = $graph_box->[0] + 5;
779
780    my $y1 = $graph_box->[1] + ($tic_distance * $count);
781
782    if ($has_columns) {
783      $y1 += $tic_distance / 2;
784    }
785
786    $img->line(x1 => $x1, x2 => $x2, y1 => $y1, y2 => $y1, aa => 1, color => '000000');
787
788    my @box = $self->_text_bbox($label, 'legend')
789      or return;
790
791    my $width = $box[2];
792    my $height = $box[3];
793
794    $img->string(%text_info,
795                 x    => ($x1 - ($width + 5)),
796                 y    => ($y1 + ($height / 2)),
797                 text => $label
798                );
799
800  }
801
802}
803
804sub _draw_x_tics {
805  my $self = shift;
806
807  my $img = $self->_get_image();
808  my $graph_box = $self->_get_graph_box();
809  my $image_box = $self->_get_image_box();
810
811  my $tic_count = $self->_get_x_tics();
812  my $min = $self->_get_min_value();
813  my $max = $self->_get_max_value();
814  my $interval = ($max - $min) / ($tic_count - 1);
815
816  # If we have columns, we want the x-ticks to show up in the middle of the column, not on the left edge
817  my $tic_distance = ($graph_box->[2] - $graph_box->[0]) / ($tic_count -1);
818
819  my %text_info = $self->_text_style('legend')
820    or return;
821
822  my $show_gridlines = $self->{_style}{features}{'vertical_gridlines'};
823  my @grid_line = $self->_get_line("vgrid");
824  for my $count (0 .. $tic_count-1) {
825    my $x1 = $graph_box->[0] + ($tic_distance * $count);
826
827    my $y1 = $graph_box->[3] + 5;
828    my $y2 = $graph_box->[3] - 5;
829
830    my $value = ($count*$interval)+$min;
831
832    $img->line(x1 => $x1, x2 => $x1, y1 => $y1, y2 => $y2, aa => 1, color => '000000');
833
834    my @box = $self->_text_bbox($value, 'legend')
835      or return;
836
837    my $width = $box[2];
838    my $height = $box[3];
839
840    $img->string(%text_info,
841                 x    => ($x1 - ($width / 2)),
842                 y    => ($y1 + $height + 5),
843                 text => $value
844                );
845
846    if ($show_gridlines && $x1 != $graph_box->[0] && $x1 != $graph_box->[2]) {
847      $self->_line(x1 => $x1, x2 => $x1,
848		   y1 => $graph_box->[1], y2 => $graph_box->[3],
849		   img => $img,
850		   @grid_line);
851    }
852  }
853}
854
855sub _valid_input {
856  my $self = shift;
857
858  if (!defined $self->_get_data_series() || !keys %{$self->_get_data_series()}) {
859    return $self->_error("No data supplied");
860  }
861
862  my $data = $self->_get_data_series();
863  if (defined $data->{'line'} && !scalar @{$data->{'line'}->[0]->{'data'}}) {
864    return $self->_error("No values in data series");
865  }
866  if (defined $data->{'column'} && !scalar @{$data->{'column'}->[0]->{'data'}}) {
867    return $self->_error("No values in data series");
868  }
869  if (defined $data->{'stacked_column'} && !scalar @{$data->{'stacked_column'}->[0]->{'data'}}) {
870    return $self->_error("No values in data series");
871  }
872
873  return 1;
874}
875
876sub _set_column_count   { $_[0]->{'column_count'} = $_[1]; }
877sub _set_min_value      { $_[0]->{'min_value'} = $_[1]; }
878sub _set_max_value      { $_[0]->{'max_value'} = $_[1]; }
879sub _set_image_box      { $_[0]->{'image_box'} = $_[1]; }
880sub _set_graph_box      { $_[0]->{'graph_box'} = $_[1]; }
881sub _set_series_counter { $_[0]->{'series_counter'} = $_[1]; }
882sub _get_column_count   { return $_[0]->{'column_count'} }
883sub _get_min_value      { return $_[0]->{'min_value'} }
884sub _get_max_value      { return $_[0]->{'max_value'} }
885sub _get_image_box      { return $_[0]->{'image_box'} }
886sub _get_graph_box      { return $_[0]->{'graph_box'} }
887sub _reset_series_counter { $_[0]->{series_counter} = 0 }
888sub _get_series_counter { return $_[0]->{'series_counter'} }
889
890sub _style_defs {
891  my ($self) = @_;
892
893  my %work = %{$self->SUPER::_style_defs()};
894  push @{$work{features}}, qw/graph_outline graph_fill linemarkers/;
895  $work{vgrid} =
896    {
897     color => "lookup(fg)",
898     style => "solid",
899    };
900
901  return \%work;
902}
903
904sub _composite {
905  my ($self) = @_;
906  return ( $self->SUPER::_composite(), "graph", "vgrid" );
907}
908
9091;
910
911