1 package Imager::Graph::Pie;
5 Imager::Graph::Pie - a tool for drawing pie charts on Imager images
9 use Imager::Graph::Pie;
11 my $chart = Imager::Graph::Pie->new;
12 # see Imager::Graph for options
13 my $img = $chart->draw(
14 data => [ $first_amount, $second_amount ],
19 Imager::Graph::Pie is intender to make it simple to use L<Imager> to
20 create good looking pie graphs.
22 Most of the basic layout and color selection is handed off to
32 @ISA = qw(Imager::Graph);
33 use Imager::Graph::Util;
36 use constant PI => 3.1415926535;
38 =item $graph->draw(...)
40 Draws a pie graph onto a new image and returns the image.
42 You must at least supply a C<data> parameter and should probably supply a C<labels> parameter. If you supply a C<labels> parameter, you must supply a C<font> parameter.
44 The C<data> parameter should be a reference to an array containing the
45 data the pie graph should present.
47 The C<labels> parameter is a reference to an array of labels,
48 corresponding to the values in C<data>.
54 As described in L<Imager::Graph> you can enable extra features for
55 your graph. The features you can use with pie graphs are:
59 =item show_callouts_onAll_segments()
61 all labels are presented as callouts
65 sub show_callouts_onAll_segments {
66 $_[0]->{'custom_style'}->{'features'}->{'allcallouts'} = 1;
69 =item show_only_label_percentages()
71 only show the percentage, not the labels.
75 sub show_only_label_percentages {
76 $_[0]->{'custom_style'}->{'features'}->{'labelspconly'} = 1;
79 =item show_label_percentages()
81 adds the percentage of the pie to each label.
85 sub show_label_percentages {
86 $_[0]->{'custom_style'}->{'features'}->{'labelspc'} = 1;
91 Additionally, arguments can be added to draw() :
97 adds a legend to your graph. Requires the labels parameter
101 labels each segment of the graph. If the label doesn't fit inside the
102 segment it is presented as a callout.
106 adds the percentage of the pie to each label.
110 the segments are labels with their percentages only.
114 all labels are presented as callouts
118 the pie segments are outlined.
122 the pie is given a drop shadow.
126 =head1 PIE CHART STYLES
128 The following style values are specific to pie charts:
130 Controlling callouts, the C<callout> option:
136 color - the color of the callout line and the callout text.
140 font, size - font and size of the callout text
144 outside - the distance the radial callout line goes outside the pie
148 leadlen - the length of the horizontal callout line from the end of
153 gap - the distance between the end of the horizontal callout line and
158 inside - the length of the radial callout line within the pie.
162 The outline, line option controls the color of the pie segment
163 outlines, if enabled with the C<outline> feature.
171 maxsegment - any segment below this fraction of the total of the
172 segments will be put into the "others" segment. Default: 0.01
176 The top level C<otherlabel> setting controls the label for the
177 "others" segment, default "(others)".
183 # from the Netcraft September 2001 web survey
184 # http://www.netcraft.com/survey/
185 my @data = qw(17874757 8146372 1321544 811406 );
186 my @labels = qw(Apache Microsoft i_planet Zeus );
188 my $pie = Imager::Graph::Pie->new;
190 First a simple graph, normal size, no labels:
192 my $img = $pie->draw(data=>\@data)
197 # error handling omitted for brevity from now on
198 $img = $pie->draw(data=>\@data, labels=>\@labels, features=>'labels');
200 just percentages in the segments:
202 $img = $pie->draw(data=>\@data, features=>'labelspconly');
204 add a legend as well:
206 $img = $pie->draw(data=>\@data, labels=>\@labels,
207 features=>[ 'labelspconly', 'legend' ]);
209 and a title, but move the legend down, and add a dropshadow:
211 $img = $pie->draw(data=>\@data, labels=>\@labels,
212 title=>'Netcraft Web Survey',
213 legend=>{ valign=>'bottom' },
214 features=>[ qw/labelspconly legend dropshadow/ ]);
216 something a bit prettier:
218 $img = $pie->draw(data=>\@data, labels=>\@labels,
219 style=>'fount_lin', features=>'legend');
221 suitable for monochrome output:
223 $img = $pie->draw(data=>\@data, labels=>\@labels,
224 style=>'mono', features=>'legend');
228 # this function is too long
230 my ($self, %opts) = @_;
232 my $data_series = $self->_get_data_series(\%opts);
234 $self->_valid_input($data_series)
237 my @data = @{$data_series->[0]->{'data'}};
239 my @labels = @{$self->_get_labels(\%opts) || []};
241 $self->_style_setup(\%opts);
243 my $style = $self->{_style};
245 my $img = $self->_make_img()
248 my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
249 if ($style->{title}{text}) {
250 $self->_draw_title($img, \@chart_box)
255 for my $item (@data) {
259 # consolidate any segments that are too small to display
260 $self->_consolidate_segments(\@data, \@labels, $total);
262 if ($style->{features}{legend} && (scalar @labels)) {
263 $self->_draw_legend($img, \@labels, \@chart_box)
267 # the following code is fairly ugly
268 # it attempts to work out a good layout for the components of the chart
272 my @ebox = (0, 0, 0, 0);
273 defined(my $callout_outside = $self->_get_number('callout.outside'))
275 defined(my $callout_leadlen = $self->_get_number('callout.leadlen'))
277 defined(my $callout_gap = $self->_get_number('callout.gap'))
279 defined(my $label_vpad = $self->_get_number('label.vpad'))
281 defined(my $label_hpad = $self->_get_number('label.hpad'))
284 int($self->_small_extent(\@chart_box) * $style->{pie}{guessfactor} * 0.5);
285 for my $data (@data) {
286 my $item = { data=>$data, index=>$index };
287 my $size = 2 * PI * $data / $total;
288 $item->{begin} = $pos;
291 if (scalar @labels) {
292 $item->{text} = $labels[$index];
294 if ($style->{features}{labelspconly}) {
296 $style->{label}{pconlyformat}->($data/$total * 100);
299 if ($style->{features}{labelspc}) {
301 $style->{label}{pcformat}->($item->{text}, $data/$total * 100);
304 elsif ($style->{features}{labelspconly}) {
306 $style->{label}{pconlyformat}->($data/$total * 100);
309 elsif ($style->{features}{labels}) {
312 $item->{callout} = 1 if $style->{features}{allcallouts};
313 if (!$item->{callout}) {
314 my @lbox = $self->_text_bbox($item->{text}, 'label')
316 $item->{lbox} = \@lbox;
317 if ($item->{label}) {
318 unless ($self->_fit_text(0, 0, 'label', $item->{text}, $guessradius,
319 $item->{begin}, $item->{end})) {
320 $item->{callout} = 1;
324 if ($item->{callout}) {
326 my @cbox = $self->_text_bbox($item->{text}, 'callout')
328 $item->{cbox} = \@cbox;
329 $item->{cangle} = ($item->{begin} + $item->{end}) / 2;
330 my $dist = cos($item->{cangle}) * ($guessradius+
332 my $co_size = $callout_leadlen + $callout_gap + $item->{cbox}[2];
334 $dist -= $co_size - $guessradius;
335 $dist < $ebox[0] and $ebox[0] = $dist;
338 $dist += $co_size - $guessradius;
339 $dist > $ebox[2] and $ebox[2] = $dist;
348 int($self->_small_extent(\@chart_box) * $style->{pie}{size} * 0.5);
349 my $max_width = $chart_box[2] - $chart_box[0] + $ebox[0] - $ebox[2];
350 if ($radius > $max_width / 2) {
351 $radius = int($max_width / 2);
353 $chart_box[0] -= $ebox[0];
354 $chart_box[2] -= $ebox[2];
355 my $cx = int(($chart_box[0] + $chart_box[2]) / 2);
356 my $cy = int(($chart_box[1] + $chart_box[3]) / 2);
357 if ($style->{features}{dropshadow}) {
358 my @shadow_fill = $self->_get_fill('dropshadow.fill')
360 my $offx = $self->_get_number('dropshadow.offx')
362 my $offy = $self->_get_number('dropshadow.offy');
363 for my $item (@info) {
364 $img->arc(x=>$cx+$offx, 'y'=>$cy+$offy, r=>$radius+1, aa => 1,
365 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
368 $self->_filter_region($img,
369 $cx+$offx-$radius-10, $cy+$offy-$radius-10,
370 $cx+$offx+$radius+10, $cy+$offy+$radius+10,
372 if $style->{dropshadow}{filter};
375 my @fill_box = ( $cx-$radius, $cy-$radius, $cx+$radius, $cy+$radius );
376 for my $item (@info) {
377 $item->{begin} < $item->{end}
379 my @fill = $self->_data_fill($item->{index}, \@fill_box)
381 $img->arc(x=>$cx, 'y'=>$cy, r=>$radius, aa => 1,
382 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
385 if ($style->{features}{outline}) {
386 my $outcolor = $self->_get_color('outline.line');
387 for my $item (@info) {
388 my $px = int($cx + $radius * cos($item->{begin}));
389 my $py = int($cy + $radius * sin($item->{begin}));
390 $item->{begin} < $item->{end}
392 $img->line(x1=>$cx, y1=>$cy, x2=>$px, y2=>$py, color=>$outcolor);
393 for (my $i = $item->{begin}; $i < $item->{end}; $i += PI/180) {
394 my $stroke_end = $i + PI/180;
395 $stroke_end = $item->{end} if $stroke_end > $item->{end};
396 my $nx = int($cx + $radius * cos($stroke_end));
397 my $ny = int($cy + $radius * sin($stroke_end));
398 $img->line(x1=>$px, y1=>$py, x2=>$nx, y2=>$ny, color=>$outcolor,
400 ($px, $py) = ($nx, $ny);
405 my $callout_inside = $radius - $self->_get_number('callout.inside');
406 $callout_outside += $radius;
409 for my $label (@info) {
410 if ($label->{label} && !$label->{callout}) {
411 # at this point we know we need the label font, to calculate
412 # whether the label will fit if anything else
413 unless (%label_text) {
414 %label_text = $self->_text_style('label')
417 my @loc = $self->_fit_text($cx, $cy, 'label', $label->{text}, $radius,
418 $label->{begin}, $label->{end});
420 my $tcx = ($loc[0]+$loc[2])/2;
421 my $tcy = ($loc[1]+$loc[3])/2;
422 #$img->box(xmin=>$loc[0], ymin=>$loc[1], xmax=>$loc[2], ymax=>$loc[3],
423 # color=>Imager::Color->new(0,0,0));
424 $img->string(%label_text, x=>$tcx-$label->{lbox}[2]/2,
425 'y'=>$tcy+$label->{lbox}[3]/2+$label->{lbox}[1],
426 text=>$label->{text}, aa => 1);
429 $label->{callout} = 1;
430 my @cbox = $self->_text_bbox($label->{text}, 'callout')
432 $label->{cbox} = \@cbox;
433 $label->{cangle} = ($label->{begin} + $label->{end}) / 2;
436 if ($label->{callout}) {
437 unless (%callout_text) {
438 %callout_text = $self->_text_style('callout')
441 my $ix = floor(0.5 + $cx + $callout_inside * cos($label->{cangle}));
442 my $iy = floor(0.5 + $cy + $callout_inside * sin($label->{cangle}));
443 my $ox = floor(0.5 + $cx + $callout_outside * cos($label->{cangle}));
444 my $oy = floor(0.5 + $cy + $callout_outside * sin($label->{cangle}));
445 my $lx = ($ox < $cx) ? $ox - $callout_leadlen : $ox + $callout_leadlen;
446 $img->line(x1=>$ix, y1=>$iy, x2=>$ox, y2=>$oy, antialias=>1,
447 color=>$self->_get_color('callout.color'));
448 $img->line(x1=>$ox, y1=>$oy, x2=>$lx, y2=>$oy, antialias=>1,
449 color=>$self->_get_color('callout.color'));
450 #my $tx = $lx + $callout_gap;
451 my $ty = $oy + $label->{cbox}[3]/2+$label->{cbox}[1];
453 $img->string(%callout_text, x=>$lx-$callout_gap-$label->{cbox}[2],
454 'y'=>$ty, text=>$label->{text}, aa=>1);
457 $img->string(%callout_text, x=>$lx+$callout_gap, 'y'=>$ty,
458 text=>$label->{text}, aa=>1);
467 my ($self, $data_series) = @_;
469 if (!defined $data_series || !scalar @$data_series) {
470 return $self->_error("No data supplied");
474 or return $self->_error("Pie charts only allow one data series");
476 my $data = $data_series->[0]{data};
478 if (!scalar @$data) {
479 return $self->_error("No values in data series");
485 for my $item (@$data) {
487 and return $self->_error("Data index $index is less than zero");
495 and return $self->_error("Sum of all data values is zero");
500 =head1 INTERNAL FUNCTIONS
502 These are used in the implementation of Imager::Graph, and are
503 documented for debuggers and developers.
507 =item _consolidate_segments($data, $labels, $total)
509 Consolidate segments that are too small into an 'others' segment.
513 sub _consolidate_segments {
514 my ($self, $data, $labels, $total) = @_;
518 for my $item (@$data) {
519 if ($item / $total < $self->{_style}{pie}{maxsegment}) {
520 push(@others, $index);
526 for my $index (reverse @others) {
527 $others += $data->[$index];
528 splice(@$labels, $index, 1);
529 splice(@$data, $index, 1);
531 push(@$labels, $self->{_style}{otherlabel}) if @$labels;
532 push(@$data, $others);
538 my ($x, $y, @l) = @_;
540 my $res = $l[0]*$x + $l[1] * $y + $l[2];
541 print "test ", (abs($res) < 0.000001) ? "success\n" : "failure $res\n";
544 =item _fit_text($cx, $cy, $name, $text, $radius, $begin, $end)
546 Attempts to fit text into a pie segment with its center at ($cx, $cy)
547 with the given radius, covering the angles $begin through $end.
549 Returns a list defining the bounding box of the text if it does fit.
554 my ($self, $cx, $cy, $name, $text, $radius, $begin, $end) = @_;
556 #print "fit: $cx, $cy '$text' $radius $begin $end\n";
557 my @tbox = $self->_text_bbox($text, $name)
559 my $tcx = floor(0.5+$cx + cos(($begin+$end)/2) * $radius *3/5);
560 my $tcy = floor(0.5+$cy + sin(($begin+$end)/2) * $radius *3/5);
561 my $topy = $tcy - $tbox[3]/2;
562 my $boty = $topy + $tbox[3];
564 for my $y ($topy, $boty) {
565 my %entry = ( 'y'=>$y );
566 $entry{line} = [ line_from_points($tcx, $y, $tcx+1, $y) ];
567 $entry{left} = -$radius;
568 $entry{right} = $radius;
569 for my $angle ($begin, $end) {
570 my $ex = $cx + cos($angle)*$radius;
571 my $ey = $cy + sin($angle)*$radius;
572 my @line = line_from_points($cx, $cy, $ex, $ey);
573 #_test_line($cx, $cy, @line);
574 #_test_line($ex, $ey, @line);
575 my $goodsign = $line[0] * $tcx + $line[1] * $tcy + $line[2];
576 for my $pos (@entry{qw/left right/}) {
577 my $sign = $line[0] * ($pos+$tcx) + $line[1] * $y + $line[2];
578 if ($goodsign * $sign < 0) {
579 if (my @p = intersect_lines(@line, @{$entry{line}})) {
580 # die "$goodsign $sign ($pos, $tcx) no intersect (@line) (@{$entry{line}})" ; # this would be wierd
581 #_test_line(@p, @line);
582 #_test_line(@p, @{$entry{line}});
592 my $dist2 = ($pos+$tcx-$cx) * ($pos+$tcx-$cx)
593 + ($y - $cy) * ($y - $cy);
594 if ($dist2 > $radius * $radius) {
596 intersect_line_and_circle(@{$entry{line}}, $cx, $cy, $radius);
598 my @p = splice(@points, 0, 2);
599 if ($p[0] < $cx && $tcx+$pos < $p[0]) {
602 elsif ($p[0] > $cx && $tcx+$pos > $p[0]) {
609 push(@lines, \%entry);
611 my $left = $lines[0]{left} > $lines[1]{left} ? $lines[0]{left} : $lines[1]{left};
612 my $right = $lines[0]{right} < $lines[1]{right} ? $lines[0]{right} : $lines[1]{right};
613 return if $right - $left < $tbox[2];
615 return ($tcx+$left, $topy, $tcx+$right, $boty);
619 ( 'pie', $_[0]->SUPER::_composite() );
625 my %work = %{$self->SUPER::_style_defs()};
626 $work{otherlabel} = "(others)";
644 Tony Cook <tony@develop-help.com>
648 Imager::Graph(3), Imager(3), perl(1)