1 package Imager::Graph::Vertical;
5 Imager::Graph::Vertical- A super class for line/bar/column/area charts
9 use Imager::Graph::Vertical;
11 my $vert = Imager::Graph::Vertical->new;
12 $vert->add_column_data_series(\@data, "My data");
13 $vert->add_area_data_series(\@data2, "Area data");
14 $vert->add_stacked_column_data_series(\@data3, "stacked data");
15 $vert->add_line_data_series(\@data4, "line data");
16 my $img = $vert->draw();
18 use Imager::Graph::Column;
19 my $column = Imager::Graph::Column->new;
20 $column->add_data_series(\@data, "my data");
21 my $img = $column->draw();
25 This is a base class that implements the functionality for column,
26 stacked column, line and area charts where the dependent variable is
27 represented in changes in the vertical position.
29 The subclasses, L<Imager::Graph::Column>,
30 L<Imager::Graph::StackedColumn>, L<Imager::Graph::Line> and
31 L<Imager::Graph::Area> simply provide default data series types.
40 @ISA = qw(Imager::Graph);
43 use constant STARTING_MIN_VALUE => 99999;
47 =item add_data_series(\@data, $series_name)
49 Add a data series to the graph, of the default type. This requires
50 that the graph object be one of the derived graph classes.
57 my $series_name = shift;
59 my $series_type = $self->_get_default_series_type();
60 $self->_add_data_series($series_type, $data_ref, $series_name);
65 =item add_column_data_series(\@data, $series_name)
67 Add a column data series to the graph.
71 sub add_column_data_series {
74 my $series_name = shift;
76 $self->_add_data_series('column', $data_ref, $series_name);
81 =item add_stacked_column_data_series(\@data, $series_name)
83 Add a stacked column data series to the graph.
87 sub add_stacked_column_data_series {
90 my $series_name = shift;
92 $self->_add_data_series('stacked_column', $data_ref, $series_name);
97 =item add_line_data_series(\@data, $series_name)
99 Add a line data series to the graph.
103 sub add_line_data_series {
105 my $data_ref = shift;
106 my $series_name = shift;
108 $self->_add_data_series('line', $data_ref, $series_name);
113 =item add_area_data_series(\@data, $series_name)
115 Add a area data series to the graph.
119 sub add_area_data_series {
121 my $data_ref = shift;
122 my $series_name = shift;
124 $self->_add_data_series('area', $data_ref, $series_name);
129 =item set_y_max($value)
131 Sets the maximum y value to be displayed. This will be ignored if the
132 y_max is lower than the highest value.
137 $_[0]->{'custom_style'}->{'y_max'} = $_[1];
140 =item set_y_min($value)
142 Sets the minimum y value to be displayed. This will be ignored if the
143 y_min is higher than the lowest value.
148 $_[0]->{'custom_style'}->{'y_min'} = $_[1];
151 =item set_column_padding($int)
153 Sets the padding between columns. This is a percentage of the column
154 width. Defaults to 0.
158 sub set_column_padding {
159 $_[0]->{'custom_style'}->{'column_padding'} = $_[1];
162 =item set_range_padding($percentage)
164 Sets the padding to be used, as a percentage. For example, if your
165 data ranges from 0 to 10, and you have a 20 percent padding, the y
168 Defaults to 10. This attribute is ignored for positive numbers if
169 set_y_max() has been called, and ignored for negative numbers if
170 set_y_min() has been called.
174 sub set_range_padding {
175 $_[0]->{'custom_style'}->{'range_padding'} = $_[1];
178 =item set_negative_background($color)
180 Sets the background color used below the x axis.
184 sub set_negative_background {
185 $_[0]->{'custom_style'}->{'negative_bg'} = $_[1];
195 my ($self, %opts) = @_;
197 if (!$self->_valid_input()) {
201 $self->_style_setup(\%opts);
203 my $style = $self->{_style};
208 my $img = $self->_get_image()
211 my @image_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
212 $self->_set_image_box(\@image_box);
214 my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
215 $self->_draw_legend(\@chart_box);
216 if ($style->{title}{text}) {
217 $self->_draw_title($img, \@chart_box)
221 # Scale the graph box down to the widest graph that can cleanly hold the # of columns.
222 return unless $self->_get_data_range();
223 $self->_remove_tics_from_chart_box(\@chart_box, \%opts);
224 my $column_count = $self->_get_column_count();
226 my $width = $self->_get_number('width');
227 my $height = $self->_get_number('height');
229 my $graph_width = $chart_box[2] - $chart_box[0];
230 my $graph_height = $chart_box[3] - $chart_box[1];
232 my $col_width = ($graph_width - 1) / $column_count;
233 if ($col_width > 1) {
234 $graph_width = int($col_width) * $column_count + 1;
237 $graph_width = $col_width * $column_count + 1;
240 my $tic_count = $self->_get_y_tics();
241 my $tic_distance = ($graph_height-1) / ($tic_count - 1);
242 $graph_height = int($tic_distance * ($tic_count - 1));
244 my $top = $chart_box[1];
245 my $left = $chart_box[0];
247 $self->{'_style'}{'graph_width'} = $graph_width;
248 $self->{'_style'}{'graph_height'} = $graph_height;
250 my @graph_box = ($left, $top, $left + $graph_width, $top + $graph_height);
251 $self->_set_graph_box(\@graph_box);
253 my @fill_box = ( $left, $top, $left+$graph_width, $top+$graph_height );
254 if ($self->_feature_enabled("graph_outline")) {
255 my @line = $self->_get_line("graph.outline")
270 $self->_get_fill('graph.fill'),
274 my $min_value = $self->_get_min_value();
275 my $max_value = $self->_get_max_value();
276 my $value_range = $max_value - $min_value;
280 $zero_position = $top + $graph_height - (-1*$min_value / $value_range) * ($graph_height-1);
283 if ($min_value < 0) {
285 color => $self->_get_color('negative_bg'),
287 xmax => $left+$graph_width- 1,
288 ymin => $zero_position,
289 ymax => $top+$graph_height - 1,
294 y1 => $zero_position,
295 x2 => $left + $graph_width,
296 y2 => $zero_position,
297 color => $self->_get_color('outline.line'),
301 $self->_reset_series_counter();
303 if ($self->_get_data_series()->{'stacked_column'}) {
304 return unless $self->_draw_stacked_columns();
306 if ($self->_get_data_series()->{'column'}) {
307 return unless $self->_draw_columns();
309 if ($self->_get_data_series()->{'line'}) {
310 return unless $self->_draw_lines();
312 if ($self->_get_data_series()->{'area'}) {
313 return unless $self->_draw_area();
316 if ($self->_get_y_tics()) {
317 $self->_draw_y_tics();
319 if ($self->_get_labels(\%opts)) {
320 $self->_draw_x_tics(\%opts);
323 return $self->_get_image();
326 sub _get_data_range {
331 my $column_count = 0;
333 my ($sc_min, $sc_max, $sc_cols) = $self->_get_stacked_column_range();
334 my ($c_min, $c_max, $c_cols) = $self->_get_column_range();
335 my ($l_min, $l_max, $l_cols) = $self->_get_line_range();
336 my ($a_min, $a_max, $a_cols) = $self->_get_area_range();
338 # These are side by side...
341 $min_value = $self->_min(STARTING_MIN_VALUE, $sc_min, $c_min, $l_min, $a_min);
342 $max_value = $self->_max(0, $sc_max, $c_max, $l_max, $a_max);
344 my $config_min = $self->_get_number('y_min');
345 my $config_max = $self->_get_number('y_max');
347 if (defined $config_max && $config_max < $max_value) {
350 if (defined $config_min && $config_min > $min_value) {
354 my $range_padding = $self->_get_number('range_padding');
355 if (defined $config_min) {
356 $min_value = $config_min;
359 if ($min_value > 0) {
362 if ($range_padding && $min_value < 0) {
363 my $difference = $min_value * $range_padding / 100;
364 if ($min_value < -1 && $difference > -1) {
367 $min_value += $difference;
370 if (defined $config_max) {
371 $max_value = $config_max;
374 if ($range_padding && $max_value > 0) {
375 my $difference = $max_value * $range_padding / 100;
376 if ($max_value > 1 && $difference < 1) {
379 $max_value += $difference;
382 $column_count = $self->_max(0, $sc_cols, $l_cols, $a_cols);
384 if ($self->_get_number('automatic_axis')) {
385 # In case this was set via a style, and not by the api method
386 eval { require Chart::Math::Axis; };
388 return $self->_error("Can't use automatic_axis - $@");
391 my $axis = Chart::Math::Axis->new();
392 $axis->include_zero();
393 $axis->add_data($min_value, $max_value);
394 $max_value = $axis->top;
395 $min_value = $axis->bottom;
396 my $ticks = $axis->ticks;
397 # The +1 is there because we have the bottom tick as well
398 $self->set_y_tics($ticks+1);
401 $self->_set_max_value($max_value);
402 $self->_set_min_value($min_value);
403 $self->_set_column_count($column_count);
412 foreach my $value (@_) {
413 next unless defined $value;
414 if ($value < $min) { $min = $value; }
423 foreach my $value (@_) {
424 next unless defined $value;
425 if ($value > $min) { $min = $value; }
430 sub _get_line_range {
432 my $series = $self->_get_data_series()->{'line'};
433 return (undef, undef, 0) unless $series;
436 my $min_value = STARTING_MIN_VALUE;
437 my $column_count = 0;
439 my @series = @{$series};
440 foreach my $series (@series) {
441 my @data = @{$series->{'data'}};
443 if (scalar @data > $column_count) {
444 $column_count = scalar @data;
447 foreach my $value (@data) {
448 if ($value > $max_value) { $max_value = $value; }
449 if ($value < $min_value) { $min_value = $value; }
453 return ($min_value, $max_value, $column_count);
456 sub _get_area_range {
458 my $series = $self->_get_data_series()->{'area'};
459 return (undef, undef, 0) unless $series;
462 my $min_value = STARTING_MIN_VALUE;
463 my $column_count = 0;
465 my @series = @{$series};
466 foreach my $series (@series) {
467 my @data = @{$series->{'data'}};
469 if (scalar @data > $column_count) {
470 $column_count = scalar @data;
473 foreach my $value (@data) {
474 if ($value > $max_value) { $max_value = $value; }
475 if ($value < $min_value) { $min_value = $value; }
479 return ($min_value, $max_value, $column_count);
483 sub _get_column_range {
486 my $series = $self->_get_data_series()->{'column'};
487 return (undef, undef, 0) unless $series;
490 my $min_value = STARTING_MIN_VALUE;
491 my $column_count = 0;
493 my @series = @{$series};
494 foreach my $series (@series) {
495 my @data = @{$series->{'data'}};
497 foreach my $value (@data) {
499 if ($value > $max_value) { $max_value = $value; }
500 if ($value < $min_value) { $min_value = $value; }
504 return ($min_value, $max_value, $column_count);
507 sub _get_stacked_column_range {
511 my $min_value = STARTING_MIN_VALUE;
512 my $column_count = 0;
514 return (undef, undef, 0) unless $self->_get_data_series()->{'stacked_column'};
515 my @series = @{$self->_get_data_series()->{'stacked_column'}};
519 for (my $i = scalar @series - 1; $i >= 0; $i--) {
520 my $series = $series[$i];
521 my $data = $series->{'data'};
523 for (my $i = 0; $i < scalar @$data; $i++) {
525 if ($data->[$i] > 0) {
526 $value = $data->[$i] + ($max_entries[$i] || 0);
527 $data->[$i] = $value;
528 $max_entries[$i] = $value;
530 elsif ($data->[$i] < 0) {
531 $value = $data->[$i] + ($min_entries[$i] || 0);
532 $data->[$i] = $value;
533 $min_entries[$i] = $value;
535 if ($value > $max_value) { $max_value = $value; }
536 if ($value < $min_value) { $min_value = $value; }
538 if (scalar @$data > $column_count) {
539 $column_count = scalar @$data;
543 return ($min_value, $max_value, $column_count);
548 my $chart_box = shift;
549 my $style = $self->{'_style'};
552 my $img = $self->_get_image();
553 if (my $series = $self->_get_data_series()->{'stacked_column'}) {
554 push @labels, map { $_->{'series_name'} } @$series;
556 if (my $series = $self->_get_data_series()->{'column'}) {
557 push @labels, map { $_->{'series_name'} } @$series;
559 if (my $series = $self->_get_data_series()->{'line'}) {
560 push @labels, map { $_->{'series_name'} } @$series;
562 if (my $series = $self->_get_data_series()->{'area'}) {
563 push @labels, map { $_->{'series_name'} } @$series;
566 if ($style->{features}{legend} && (scalar @labels)) {
567 $self->SUPER::_draw_legend($self->_get_image(), \@labels, $chart_box)
573 sub _draw_flat_legend {
579 my $style = $self->{'_style'};
581 my $img = $self->_get_image();
583 my $max_value = $self->_get_max_value();
584 my $min_value = $self->_get_min_value();
585 my $column_count = $self->_get_column_count();
587 my $value_range = $max_value - $min_value;
589 my $width = $self->_get_number('width');
590 my $height = $self->_get_number('height');
592 my $graph_width = $self->_get_number('graph_width');
593 my $graph_height = $self->_get_number('graph_height');
595 my $line_series = $self->_get_data_series()->{'line'};
596 my $series_counter = $self->_get_series_counter() || 0;
598 my $has_columns = (defined $self->_get_data_series()->{'column'} || $self->_get_data_series->{'stacked_column'}) ? 1 : 0;
600 my $col_width = int($graph_width / $column_count) -1;
602 my $graph_box = $self->_get_graph_box();
603 my $left = $graph_box->[0] + 1;
604 my $bottom = $graph_box->[1];
606 my $zero_position = $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height - 1);
608 my $line_aa = $self->_get_number("lineaa");
609 foreach my $series (@$line_series) {
610 my @data = @{$series->{'data'}};
611 my $data_size = scalar @data;
615 $interval = $graph_width / ($data_size);
618 $interval = $graph_width / ($data_size - 1);
620 my $color = $self->_data_color($series_counter);
622 # We need to add these last, otherwise the next line segment will overwrite half of the marker
623 my @marker_positions;
624 for (my $i = 0; $i < $data_size - 1; $i++) {
625 my $x1 = $left + $i * $interval;
626 my $x2 = $left + ($i + 1) * $interval;
628 $x1 += $has_columns * $interval / 2;
629 $x2 += $has_columns * $interval / 2;
631 my $y1 = $bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height;
632 my $y2 = $bottom + ($value_range - $data[$i + 1] + $min_value)/$value_range * $graph_height;
634 push @marker_positions, [$x1, $y1];
635 $img->line(x1 => $x1, y1 => $y1, x2 => $x2, y2 => $y2, aa => $line_aa, color => $color) || die $img->errstr;
638 my $x2 = $left + ($data_size - 1) * $interval;
639 $x2 += $has_columns * $interval / 2;
641 my $y2 = $bottom + ($value_range - $data[$data_size - 1] + $min_value)/$value_range * $graph_height;
643 if ($self->_feature_enabled("linemarkers")) {
644 push @marker_positions, [$x2, $y2];
645 foreach my $position (@marker_positions) {
646 $self->_draw_line_marker($position->[0], $position->[1], $series_counter);
652 $self->_set_series_counter($series_counter);
656 sub _area_data_fill {
657 my ($self, $index, $box) = @_;
659 my %fill = $self->_data_fill($index, $box);
661 my $opacity = $self->_get_number("area.opacity");
665 my $orig_fill = $fill{fill};
666 unless ($orig_fill) {
667 $orig_fill = Imager::Fill->new
669 solid => $fill{color},
675 fill => Imager::Fill->new
686 my $style = $self->{'_style'};
688 my $img = $self->_get_image();
690 my $max_value = $self->_get_max_value();
691 my $min_value = $self->_get_min_value();
692 my $column_count = $self->_get_column_count();
694 my $value_range = $max_value - $min_value;
696 my $width = $self->_get_number('width');
697 my $height = $self->_get_number('height');
699 my $graph_width = $self->_get_number('graph_width');
700 my $graph_height = $self->_get_number('graph_height');
702 my $area_series = $self->_get_data_series()->{'area'};
703 my $series_counter = $self->_get_series_counter() || 0;
705 my $col_width = int($graph_width / $column_count) -1;
707 my $graph_box = $self->_get_graph_box();
708 my $left = $graph_box->[0] + 1;
709 my $bottom = $graph_box->[1];
710 my $right = $graph_box->[2];
711 my $top = $graph_box->[3];
713 my $zero_position = $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height - 1);
715 my $line_aa = $self->_get_number("lineaa");
716 foreach my $series (@$area_series) {
717 my @data = @{$series->{'data'}};
718 my $data_size = scalar @data;
720 my $interval = $graph_width / ($data_size - 1);
722 my $color = $self->_data_color($series_counter);
724 # We need to add these last, otherwise the next line segment will overwrite half of the marker
725 my @marker_positions;
727 for (my $i = 0; $i < $data_size - 1; $i++) {
728 my $x1 = $left + $i * $interval;
730 my $y1 = $bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height;
733 push @polygon_points, [$x1, $top];
735 push @polygon_points, [$x1, $y1];
737 push @marker_positions, [$x1, $y1];
740 my $x2 = $left + ($data_size - 1) * $interval;
742 my $y2 = $bottom + ($value_range - $data[$data_size - 1] + $min_value)/$value_range * $graph_height;
743 push @polygon_points, [$x2, $y2];
744 push @polygon_points, [$x2, $top];
745 push @polygon_points, $polygon_points[0];
747 my @fill = $self->_area_data_fill($series_counter, [$left, $bottom, $right, $top]);
748 $img->polygon(points => [@polygon_points], @fill);
750 if ($self->_feature_enabled("areamarkers")) {
751 push @marker_positions, [$x2, $y2];
752 foreach my $position (@marker_positions) {
753 $self->_draw_line_marker($position->[0], $position->[1], $series_counter);
759 $self->_set_series_counter($series_counter);
765 my $style = $self->{'_style'};
767 my $img = $self->_get_image();
769 my $max_value = $self->_get_max_value();
770 my $min_value = $self->_get_min_value();
771 my $column_count = $self->_get_column_count();
773 my $value_range = $max_value - $min_value;
775 my $width = $self->_get_number('width');
776 my $height = $self->_get_number('height');
778 my $graph_width = $self->_get_number('graph_width');
779 my $graph_height = $self->_get_number('graph_height');
782 my $graph_box = $self->_get_graph_box();
783 my $left = $graph_box->[0] + 1;
784 my $bottom = $graph_box->[1];
785 my $zero_position = int($bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height -1));
787 my $bar_width = $graph_width / $column_count;
790 if ($style->{'features'}{'outline'}) {
791 $outline_color = $self->_get_color('outline.line');
794 my $series_counter = $self->_get_series_counter() || 0;
795 my $col_series = $self->_get_data_series()->{'column'};
796 my $column_padding_percent = $self->_get_number('column_padding') || 0;
797 my $column_padding = int($column_padding_percent * $bar_width / 100);
799 # 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.
800 my $column_series = 0;
802 # If there are stacked columns, non-stacked columns need to start one to the right of where they would otherwise
803 my $has_stacked_columns = (defined $self->_get_data_series()->{'stacked_column'} ? 1 : 0);
805 for (my $series_pos = 0; $series_pos < scalar @$col_series; $series_pos++) {
806 my $series = $col_series->[$series_pos];
807 my @data = @{$series->{'data'}};
808 my $data_size = scalar @data;
809 for (my $i = 0; $i < $data_size; $i++) {
810 my $part1 = $bar_width * (scalar @$col_series * $i);
811 my $part2 = ($series_pos) * $bar_width;
812 my $x1 = $left + $part1 + $part2;
813 if ($has_stacked_columns) {
814 $x1 += ($bar_width * ($i+1));
818 my $x2 = int($x1 + $bar_width - $column_padding)-1;
819 # Special case for when bar_width is less than 1.
824 my $y1 = int($bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height);
826 my $color = $self->_data_color($series_counter);
829 my @fill = $self->_data_fill($series_counter, [$x1, $y1, $x2, $zero_position-1]);
830 $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position-1, @fill);
831 if ($style->{'features'}{'outline'}) {
832 $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position, color => $outline_color);
836 my @fill = $self->_data_fill($series_counter, [$x1, $zero_position+1, $x2, $y1]);
837 $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1, @fill);
838 if ($style->{'features'}{'outline'}) {
839 $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1+1, color => $outline_color);
847 $self->_set_series_counter($series_counter);
851 sub _draw_stacked_columns {
853 my $style = $self->{'_style'};
855 my $img = $self->_get_image();
857 my $max_value = $self->_get_max_value();
858 my $min_value = $self->_get_min_value();
859 my $column_count = $self->_get_column_count();
860 my $value_range = $max_value - $min_value;
862 my $graph_box = $self->_get_graph_box();
863 my $left = $graph_box->[0] + 1;
864 my $bottom = $graph_box->[1];
866 my $graph_width = $self->_get_number('graph_width');
867 my $graph_height = $self->_get_number('graph_height');
869 my $bar_width = $graph_width / $column_count;
870 my $column_series = 0;
871 if (my $column_series_data = $self->_get_data_series()->{'column'}) {
872 $column_series = (scalar @$column_series_data);
876 my $column_padding_percent = $self->_get_number('column_padding') || 0;
877 if ($column_padding_percent < 0) {
878 return $self->_error("Column padding less than 0");
880 if ($column_padding_percent > 100) {
881 return $self->_error("Column padding greater than 0");
883 my $column_padding = int($column_padding_percent * $bar_width / 100);
886 if ($style->{'features'}{'outline'}) {
887 $outline_color = $self->_get_color('outline.line');
890 my $zero_position = $bottom + $graph_height - (-1*$min_value / $value_range) * ($graph_height -1);
891 my $col_series = $self->_get_data_series()->{'stacked_column'};
892 my $series_counter = $self->_get_series_counter() || 0;
894 foreach my $series (@$col_series) {
895 my @data = @{$series->{'data'}};
896 my $data_size = scalar @data;
897 for (my $i = 0; $i < $data_size; $i++) {
898 my $part1 = $bar_width * $i * $column_series;
900 my $x1 = int($left + $part1 + $part2);
901 my $x2 = int($x1 + $bar_width - $column_padding) - 1;
902 # Special case for when bar_width is less than 1.
907 my $y1 = int($bottom + ($value_range - $data[$i] + $min_value)/$value_range * $graph_height);
910 my @fill = $self->_data_fill($series_counter, [$x1, $y1, $x2, $zero_position-1]);
911 $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position-1, @fill);
912 if ($style->{'features'}{'outline'}) {
913 $img->box(xmin => $x1, xmax => $x2, ymin => $y1, ymax => $zero_position, color => $outline_color);
917 my @fill = $self->_data_fill($series_counter, [$x1, $zero_position+1, $x2, $y1]);
918 $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1, @fill);
919 if ($style->{'features'}{'outline'}) {
920 $img->box(xmin => $x1, xmax => $x2, ymin => $zero_position+1, ymax => $y1+1, color => $outline_color);
927 $self->_set_series_counter($series_counter);
931 sub _add_data_series {
933 my $series_type = shift;
934 my $data_ref = shift;
935 my $series_name = shift;
937 my $graph_data = $self->{'graph_data'} || {};
939 my $series = $graph_data->{$series_type} || [];
941 push @$series, { data => $data_ref, series_name => $series_name };
943 $graph_data->{$series_type} = $series;
945 $self->{'graph_data'} = $graph_data;
955 =item show_horizontal_gridlines()
957 Feature: horizontal_gridlines
958 X<horizontal_gridlines>X<features, horizontal_gridlines>
960 Enables the C<horizontal_gridlines> feature, which shows horizontal
961 gridlines at the y-tics.
963 The style of the gridlines can be controlled with the
964 set_horizontal_gridline_style() method (or by setting the hgrid
969 sub show_horizontal_gridlines {
970 $_[0]->{'custom_style'}{features}{'horizontal_gridlines'} = 1;
973 =item set_horizontal_gridline_style(style => $style, color => $color)
976 X<hgrid>X<style parameters, hgrid>
978 Set the style and color of horizonal gridlines.
980 See: L<Imager::Graph/"Line styles">
984 sub set_horizontal_gridline_style {
985 my ($self, %opts) = @_;
987 $self->{custom_style}{hgrid} ||= {};
988 @{$self->{custom_style}{hgrid}}{keys %opts} = values %opts;
993 =item show_graph_outline($flag)
995 Feature: graph_outline
996 X<graph_outline>X<features, graph_outline>
998 If no flag is supplied, unconditionally enable the graph outline.
1000 If $flag is supplied, enable/disable the graph_outline feature based
1007 sub show_graph_outline {
1008 my ($self, $flag) = @_;
1010 @_ == 1 and $flag = 1;
1012 $self->{custom_style}{features}{graph_outline} = $flag;
1017 =item set_graph_outline_style(color => ...)
1019 =item set_graph_outline_style(style => ..., color => ...)
1021 Style: graph.outline
1022 X<graph.outline>X<style parameters, graph.outline>
1024 Sets the style of the graph outline.
1026 Default: the style C<fg>.
1030 sub set_graph_outline_style {
1031 my ($self, %opts) = @_;
1033 $self->{custom_style}{graph}{outline} = \%opts;
1038 =item set_graph_fill_style(I<fill parameters>)
1041 X<graph.fill>X<style parameters, graph.fill>
1043 Set the fill used to fill the graph data area.
1045 Default: the style C<bg>.
1049 $graph->set_graph_fill_style(solid => "FF000020", combine => "normal");
1053 sub set_graph_fill_style {
1054 my ($self, %opts) = @_;
1056 $self->{custom_style}{graph}{fill} = \%opts;
1061 =item show_area_markers()
1063 =item show_area_markers($value)
1065 Feature: areamarkers.
1067 If $value is missing or true, draw markers along the top of area data
1072 $chart->show_area_markers();
1076 sub show_area_markers {
1077 my ($self, $value) = @_;
1079 @_ > 1 or $value = 1;
1081 $self->{custom_style}{features}{areamarkers} = $value;
1086 =item show_line_markers()
1088 =item show_line_markers($value)
1090 Feature: linemarkers.
1092 If $value is missing or true, draw markers on a line data series.
1094 Note: line markers are drawn by default.
1098 sub show_line_markers {
1099 my ($self, $value) = @_;
1101 @_ > 1 or $value = 1;
1103 $self->{custom_style}{features}{linemarkers} = $value;
1108 =item use_automatic_axis()
1110 Automatically scale the Y axis, based on L<Chart::Math::Axis>. If
1111 Chart::Math::Axis isn't installed, this sets an error and returns
1112 undef. Returns 1 if it is installed.
1116 sub use_automatic_axis {
1117 eval { require Chart::Math::Axis; };
1119 return $_[0]->_error("use_automatic_axis - $@\nCalled from ".join(' ', caller)."\n");
1121 $_[0]->{'custom_style'}->{'automatic_axis'} = 1;
1125 =item set_y_tics($count)
1127 Set the number of Y tics to use. Their value and position will be
1128 determined by the data range.
1133 $_[0]->{'y_tics'} = $_[1];
1137 return $_[0]->{'y_tics'} || 0;
1140 sub _remove_tics_from_chart_box {
1141 my ($self, $chart_box, $opts) = @_;
1144 my $tic_width = $self->_get_y_tic_width() || 10;
1145 my @y_tic_box = ($chart_box->[0], $chart_box->[1], $chart_box->[0] + $tic_width, $chart_box->[3]);
1148 my $tic_height = $self->_get_x_tic_height($opts) || 10;
1149 my @x_tic_box = ($chart_box->[0], $chart_box->[3] - $tic_height, $chart_box->[2], $chart_box->[3]);
1151 $self->_remove_box($chart_box, \@y_tic_box);
1152 $self->_remove_box($chart_box, \@x_tic_box);
1154 # If there's no title, the y-tics will be part off-screen. Half of the x-tic height should be more than sufficient.
1155 my @y_tic_tops = ($chart_box->[0], $chart_box->[1], $chart_box->[2], $chart_box->[1] + int($tic_height / 2));
1156 $self->_remove_box($chart_box, \@y_tic_tops);
1158 # Make sure that the first and last label fit
1159 if (my $labels = $self->_get_labels($opts)) {
1160 if (my @box = $self->_text_bbox($labels->[0], 'legend')) {
1161 my @remove_box = ($chart_box->[0],
1163 $chart_box->[0] + int($box[2] / 2) + 1,
1167 $self->_remove_box($chart_box, \@remove_box);
1169 if (my @box = $self->_text_bbox($labels->[-1], 'legend')) {
1170 my @remove_box = ($chart_box->[2] - int($box[2] / 2) - 1,
1176 $self->_remove_box($chart_box, \@remove_box);
1181 sub _get_y_tic_width {
1183 my $min = $self->_get_min_value();
1184 my $max = $self->_get_max_value();
1185 my $tic_count = $self->_get_y_tics();
1187 my $interval = ($max - $min) / ($tic_count - 1);
1189 my %text_info = $self->_text_style('legend')
1193 for my $count (0 .. $tic_count - 1) {
1194 my $value = ($count*$interval)+$min;
1196 if ($interval < 1 || ($value != int($value))) {
1197 $value = sprintf("%.2f", $value);
1199 my @box = $self->_text_bbox($value, 'legend');
1200 my $width = $box[2] - $box[0];
1204 if ($width > $max_width) {
1205 $max_width = $width;
1212 sub _get_x_tic_height {
1213 my ($self, $opts) = @_;
1215 my $labels = $self->_get_labels($opts);
1221 my $tic_count = (scalar @$labels) - 1;
1223 my %text_info = $self->_text_style('legend')
1227 for my $count (0 .. $tic_count) {
1228 my $label = $labels->[$count];
1230 my @box = $self->_text_bbox($label, 'legend');
1232 my $height = $box[3] - $box[1];
1236 if ($height > $max_height) {
1237 $max_height = $height;
1246 my $min = $self->_get_min_value();
1247 my $max = $self->_get_max_value();
1248 my $tic_count = $self->_get_y_tics();
1250 my $img = $self->_get_image();
1251 my $graph_box = $self->_get_graph_box();
1252 my $image_box = $self->_get_image_box();
1254 my $interval = ($max - $min) / ($tic_count - 1);
1256 my %text_info = $self->_text_style('legend')
1259 my $line_style = $self->_get_color('outline.line');
1260 my $show_gridlines = $self->{_style}{features}{'horizontal_gridlines'};
1261 my @grid_line = $self->_get_line("hgrid");
1262 my $tic_distance = ($graph_box->[3] - $graph_box->[1]) / ($tic_count - 1);
1263 for my $count (0 .. $tic_count - 1) {
1264 my $x1 = $graph_box->[0] - 5;
1265 my $x2 = $graph_box->[0] + 5;
1266 my $y1 = int($graph_box->[3] - ($count * $tic_distance));
1268 my $value = ($count*$interval)+$min;
1269 if ($interval < 1 || ($value != int($value))) {
1270 $value = sprintf("%.2f", $value);
1273 my @box = $self->_text_bbox($value, 'legend')
1276 $img->line(x1 => $x1, x2 => $x2, y1 => $y1, y2 => $y1, aa => 1, color => $line_style);
1278 my $width = $box[2];
1279 my $height = $box[3];
1281 $img->string(%text_info,
1282 x => ($x1 - $width - 3),
1283 y => ($y1 + ($height / 2)),
1287 if ($show_gridlines && $y1 != $graph_box->[1] && $y1 != $graph_box->[3]) {
1288 $self->_line(x1 => $graph_box->[0], y1 => $y1,
1289 x2 => $graph_box->[2], y2 => $y1,
1298 my ($self, $opts) = @_;
1300 my $img = $self->_get_image();
1301 my $graph_box = $self->_get_graph_box();
1302 my $image_box = $self->_get_image_box();
1304 my $labels = $self->_get_labels($opts);
1306 my $tic_count = (scalar @$labels) - 1;
1308 my $has_columns = (defined $self->_get_data_series()->{'column'} || defined $self->_get_data_series()->{'stacked_column'});
1310 # If we have columns, we want the x-ticks to show up in the middle of the column, not on the left edge
1311 my $denominator = $tic_count;
1315 my $tic_distance = ($graph_box->[2] - $graph_box->[0]) / ($denominator);
1316 my %text_info = $self->_text_style('legend')
1319 # If automatic axis is turned on, let's be selective about what labels we draw.
1322 if ($self->_get_number('automatic_axis')) {
1323 foreach my $label (@$labels) {
1324 my @box = $self->_text_bbox($label, 'legend');
1325 if ($box[2] > $max_size) {
1326 $max_size = $box[2];
1330 # Give the max_size some padding...
1333 $tic_skip = int($max_size / $tic_distance) + 1;
1336 my $line_style = $self->_get_color('outline.line');
1338 for my $count (0 .. $tic_count) {
1339 next if ($count % ($tic_skip + 1));
1340 my $label = $labels->[$count];
1341 my $x1 = $graph_box->[0] + ($tic_distance * $count);
1344 $x1 += $tic_distance / 2;
1349 my $y1 = $graph_box->[3] + 5;
1350 my $y2 = $graph_box->[3] - 5;
1352 $img->line(x1 => $x1, x2 => $x1, y1 => $y1, y2 => $y2, aa => 1, color => $line_style);
1354 my @box = $self->_text_bbox($label, 'legend')
1357 my $width = $box[2];
1358 my $height = $box[3];
1360 $img->string(%text_info,
1361 x => ($x1 - ($width / 2)),
1362 y => ($y1 + ($height + 5)),
1372 if (!defined $self->_get_data_series() || !keys %{$self->_get_data_series()}) {
1373 return $self->_error("No data supplied");
1376 my $data = $self->_get_data_series();
1377 if (defined $data->{'line'} && !scalar @{$data->{'line'}->[0]->{'data'}}) {
1378 return $self->_error("No values in data series");
1380 if (defined $data->{'column'} && !scalar @{$data->{'column'}->[0]->{'data'}}) {
1381 return $self->_error("No values in data series");
1383 if (defined $data->{'stacked_column'} && !scalar @{$data->{'stacked_column'}->[0]->{'data'}}) {
1384 return $self->_error("No values in data series");
1390 sub _set_column_count { $_[0]->{'column_count'} = $_[1]; }
1391 sub _set_min_value { $_[0]->{'min_value'} = $_[1]; }
1392 sub _set_max_value { $_[0]->{'max_value'} = $_[1]; }
1393 sub _set_image_box { $_[0]->{'image_box'} = $_[1]; }
1394 sub _set_graph_box { $_[0]->{'graph_box'} = $_[1]; }
1395 sub _set_series_counter { $_[0]->{'series_counter'} = $_[1]; }
1396 sub _get_column_count { return $_[0]->{'column_count'} }
1397 sub _get_min_value { return $_[0]->{'min_value'} }
1398 sub _get_max_value { return $_[0]->{'max_value'} }
1399 sub _get_image_box { return $_[0]->{'image_box'} }
1400 sub _get_graph_box { return $_[0]->{'graph_box'} }
1401 sub _reset_series_counter { $_[0]->{series_counter} = 0 }
1402 sub _get_series_counter { return $_[0]->{'series_counter'} }
1407 my %work = %{$self->SUPER::_style_defs()};
1412 push @{$work{features}}, qw/graph_outline graph_fill linemarkers/;
1415 color => "lookup(fg)",
1424 return ( $self->SUPER::_composite(), "graph", "hgrid" );