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:
61 adds a legend to your graph. Requires the labels parameter
65 labels each segment of the graph. If the label doesn't fit inside the
66 segment it is presented as a callout.
70 adds the percentage of the pie to each label.
74 the segments are labels with their percentages only.
78 all labels are presented as callouts
82 the pie segments are outlined.
86 the pie is given a drop shadow.
90 =head1 PIE CHART STYLES
92 The following style values are specific to pie charts:
94 Controlling callouts, the C<callout> option:
100 color - the color of the callout line and the callout text.
104 font, size - font and size of the callout text
108 outside - the distance the radial callout line goes outside the pie
112 leadlen - the length of the horizontal callout line from the end of
117 gap - the distance between the end of the horizontal callout line and
122 inside - the length of the radial callout line within the pie.
126 The outline, line option controls the color of the pie segment
127 outlines, if enabled with the C<outline> feature.
135 maxsegment - any segment below this fraction of the total of the
136 segments will be put into the "others" segment. Default: 0.01
140 The top level C<otherlabel> setting controls the label for the
141 "others" segment, default "(others)".
147 # from the Netcraft September 2001 web survey
148 # http://www.netcraft.com/survey/
149 my @data = qw(17874757 8146372 1321544 811406 );
150 my @labels = qw(Apache Microsoft iPlanet Zeus );
152 my $pie = Imager::Graph::Pie->new;
154 First a simple graph, normal size, no labels:
156 my $img = $pie->draw(data=>\@data)
161 # error handling omitted for brevity from now on
162 $img = $pie->draw(data=>\@data, labels=>\@labels, features=>'labels');
164 just percentages in the segments:
166 $img = $pie->draw(data=>\@data, features=>'labelspconly');
168 add a legend as well:
170 $img = $pie->draw(data=>\@data, labels=>\@labels,
171 features=>[ 'labelspconly', 'legend' ]);
173 and a title, but move the legend down, and add a dropshadow:
175 $img = $pie->draw(data=>\@data, labels=>\@labels,
176 title=>'Netcraft Web Survey',
177 legend=>{ valign=>'bottom' },
178 features=>[ qw/labelspconly legend dropshadow/ ]);
180 something a bit prettier:
182 $img = $pie->draw(data=>\@data, labels=>\@labels,
183 style=>'fount_lin', features=>'legend');
185 suitable for monochrome output:
187 $img = $pie->draw(data=>\@data, labels=>\@labels,
188 style=>'mono', features=>'legend');
192 # this function is too long
194 my ($self, %opts) = @_;
197 or return $self->_error("No data parameter supplied");
198 my @data = @{$opts{data}}
199 or return $self->_error("No values in the data parameter");
201 @labels = @{$opts{labels}} if $opts{labels};
206 for my $item (@data) {
208 and return $self->_error("Data index $index is less than zero");
216 and return $self->_error("Sum of all data values is zero");
218 $self->_style_setup(\%opts);
220 my $style = $self->{_style};
222 my $img = $self->_make_img()
225 my @chart_box = ( 0, 0, $img->getwidth-1, $img->getheight-1 );
226 if ($style->{title}{text}) {
227 $self->_draw_title($img, \@chart_box)
231 # consolidate any segments that are too small to display
232 $self->_consolidate_segments(\@data, \@labels, $total);
234 if ($style->{features}{legend} && $opts{labels}) {
235 $self->_draw_legend($img, \@labels, \@chart_box)
239 # the following code is fairly ugly
240 # it attempts to work out a good layout for the components of the chart
244 my @ebox = (0, 0, 0, 0);
245 defined(my $callout_outside = $self->_get_number('callout.outside'))
247 defined(my $callout_leadlen = $self->_get_number('callout.leadlen'))
249 defined(my $callout_gap = $self->_get_number('callout.gap'))
251 defined(my $label_vpad = $self->_get_number('label.vpad'))
253 defined(my $label_hpad = $self->_get_number('label.hpad'))
256 int($self->_small_extent(\@chart_box) * $style->{pie}{guessfactor} * 0.5);
257 for my $data (@data) {
258 my $item = { data=>$data, index=>$index };
259 my $size = 2 * PI * $data / $total;
260 $item->{begin} = $pos;
264 $item->{text} = $labels[$index];
266 if ($style->{features}{labelspconly}) {
268 $style->{label}{pconlyformat}->($data/$total * 100);
271 if ($style->{features}{labelspc}) {
273 $style->{label}{pcformat}->($item->{text}, $data/$total * 100);
276 elsif ($style->{features}{labelspconly}) {
278 $style->{label}{pconlyformat}->($data/$total * 100);
281 elsif ($style->{features}{labels}) {
284 $item->{callout} = 1 if $style->{features}{allcallouts};
285 if (!$item->{callout}) {
286 my @lbox = $self->_text_bbox($item->{text}, 'label')
288 $item->{lbox} = \@lbox;
289 if ($item->{label}) {
290 unless ($self->_fit_text(0, 0, 'label', $item->{text}, $guessradius,
291 $item->{begin}, $item->{end})) {
292 $item->{callout} = 1;
296 if ($item->{callout}) {
298 my @cbox = $self->_text_bbox($item->{text}, 'callout')
300 $item->{cbox} = \@cbox;
301 $item->{cangle} = ($item->{begin} + $item->{end}) / 2;
302 my $dist = cos($item->{cangle}) * ($guessradius+
304 my $co_size = $callout_leadlen + $callout_gap + $item->{cbox}[2];
306 $dist -= $co_size - $guessradius;
307 $dist < $ebox[0] and $ebox[0] = $dist;
310 $dist += $co_size - $guessradius;
311 $dist > $ebox[2] and $ebox[2] = $dist;
320 int($self->_small_extent(\@chart_box) * $style->{pie}{size} * 0.5);
321 my $max_width = $chart_box[2] - $chart_box[0] + $ebox[0] - $ebox[2];
322 if ($radius > $max_width / 2) {
323 $radius = int($max_width / 2);
325 $chart_box[0] -= $ebox[0];
326 $chart_box[2] -= $ebox[2];
327 my $cx = int(($chart_box[0] + $chart_box[2]) / 2);
328 my $cy = int(($chart_box[1] + $chart_box[3]) / 2);
329 if ($style->{features}{dropshadow}) {
330 my @shadow_fill = $self->_get_fill('dropshadow.fill')
332 my $offx = $self->_get_number('dropshadow.offx')
334 my $offy = $self->_get_number('dropshadow.offy');
335 for my $item (@info) {
336 $img->arc(x=>$cx+$offx, 'y'=>$cy+$offy, r=>$radius+1, aa => 1,
337 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
340 $self->_filter_region($img,
341 $cx+$offx-$radius-10, $cy+$offy-$radius-10,
342 $cx+$offx+$radius+10, $cy+$offy+$radius+10,
344 if $style->{dropshadow}{filter};
347 my @fill_box = ( $cx-$radius, $cy-$radius, $cx+$radius, $cy+$radius );
348 for my $item (@info) {
349 $item->{begin} < $item->{end}
351 my @fill = $self->_data_fill($item->{index}, \@fill_box)
353 $img->arc(x=>$cx, 'y'=>$cy, r=>$radius, aa => 1,
354 d1=>180/PI * $item->{begin}, d2=>180/PI * $item->{end},
357 if ($style->{features}{outline}) {
358 my $outcolor = $self->_get_color('outline.line');
359 for my $item (@info) {
360 my $px = int($cx + $radius * cos($item->{begin}));
361 my $py = int($cy + $radius * sin($item->{begin}));
362 $item->{begin} < $item->{end}
364 $img->line(x1=>$cx, y1=>$cy, x2=>$px, y2=>$py, color=>$outcolor);
365 for (my $i = $item->{begin}; $i < $item->{end}; $i += PI/180) {
366 my $stroke_end = $i + PI/180;
367 $stroke_end = $item->{end} if $stroke_end > $item->{end};
368 my $nx = int($cx + $radius * cos($stroke_end));
369 my $ny = int($cy + $radius * sin($stroke_end));
370 $img->line(x1=>$px, y1=>$py, x2=>$nx, y2=>$ny, color=>$outcolor,
372 ($px, $py) = ($nx, $ny);
377 my $callout_inside = $radius - $self->_get_number('callout.inside');
378 $callout_outside += $radius;
381 for my $label (@info) {
382 if ($label->{label} && !$label->{callout}) {
383 # at this point we know we need the label font, to calculate
384 # whether the label will fit if anything else
385 unless (%label_text) {
386 %label_text = $self->_text_style('label')
389 my @loc = $self->_fit_text($cx, $cy, 'label', $label->{text}, $radius,
390 $label->{begin}, $label->{end});
392 my $tcx = ($loc[0]+$loc[2])/2;
393 my $tcy = ($loc[1]+$loc[3])/2;
394 #$img->box(xmin=>$loc[0], ymin=>$loc[1], xmax=>$loc[2], ymax=>$loc[3],
395 # color=>Imager::Color->new(0,0,0));
396 $img->string(%label_text, x=>$tcx-$label->{lbox}[2]/2,
397 'y'=>$tcy+$label->{lbox}[3]/2+$label->{lbox}[1],
398 text=>$label->{text});
401 $label->{callout} = 1;
402 my @cbox = $self->_text_bbox($label->{text}, 'callout')
404 $label->{cbox} = \@cbox;
405 $label->{cangle} = ($label->{begin} + $label->{end}) / 2;
408 if ($label->{callout}) {
409 unless (%callout_text) {
410 %callout_text = $self->_text_style('callout')
413 my $ix = floor(0.5 + $cx + $callout_inside * cos($label->{cangle}));
414 my $iy = floor(0.5 + $cy + $callout_inside * sin($label->{cangle}));
415 my $ox = floor(0.5 + $cx + $callout_outside * cos($label->{cangle}));
416 my $oy = floor(0.5 + $cy + $callout_outside * sin($label->{cangle}));
417 my $lx = ($ox < $cx) ? $ox - $callout_leadlen : $ox + $callout_leadlen;
418 $img->line(x1=>$ix, y1=>$iy, x2=>$ox, y2=>$oy, antialias=>1,
419 color=>$self->_get_color('callout.color'));
420 $img->line(x1=>$ox, y1=>$oy, x2=>$lx, y2=>$oy, antialias=>1,
421 color=>$self->_get_color('callout.color'));
422 #my $tx = $lx + $callout_gap;
423 my $ty = $oy + $label->{cbox}[3]/2+$label->{cbox}[1];
425 $img->string(%callout_text, x=>$lx-$callout_gap-$label->{cbox}[2],
426 'y'=>$ty, text=>$label->{text});
429 $img->string(%callout_text, x=>$lx+$callout_gap, 'y'=>$ty,
430 text=>$label->{text});
438 =head1 INTERNAL FUNCTIONS
440 These are used in the implementation of Imager::Graph, and are
441 documented for debuggers and developers.
445 =item _consolidate_segments($data, $labels, $total)
447 Consolidate segments that are too small into an 'others' segment.
451 sub _consolidate_segments {
452 my ($self, $data, $labels, $total) = @_;
456 for my $item (@$data) {
457 if ($item / $total < $self->{_style}{pie}{maxsegment}) {
458 push(@others, $index);
464 for my $index (reverse @others) {
465 $others += $data->[$index];
466 splice(@$labels, $index, 1);
467 splice(@$data, $index, 1);
469 push(@$labels, $self->{_style}{otherlabel}) if @$labels;
470 push(@$data, $others);
476 my ($x, $y, @l) = @_;
478 my $res = $l[0]*$x + $l[1] * $y + $l[2];
479 print "test ", (abs($res) < 0.000001) ? "success\n" : "failure $res\n";
482 =item _fit_text($cx, $cy, $name, $text, $radius, $begin, $end)
484 Attempts to fit text into a pie segment with its center at ($cx, $cy)
485 with the given radius, covering the angles $begin through $end.
487 Returns a list defining the bounding box of the text if it does fit.
492 my ($self, $cx, $cy, $name, $text, $radius, $begin, $end) = @_;
494 #print "fit: $cx, $cy '$text' $radius $begin $end\n";
495 my @tbox = $self->_text_bbox($text, $name)
497 my $tcx = floor(0.5+$cx + cos(($begin+$end)/2) * $radius *3/5);
498 my $tcy = floor(0.5+$cy + sin(($begin+$end)/2) * $radius *3/5);
499 my $topy = $tcy - $tbox[3]/2;
500 my $boty = $topy + $tbox[3];
502 for my $y ($topy, $boty) {
503 my %entry = ( 'y'=>$y );
504 $entry{line} = [ line_from_points($tcx, $y, $tcx+1, $y) ];
505 $entry{left} = -$radius;
506 $entry{right} = $radius;
507 for my $angle ($begin, $end) {
508 my $ex = $cx + cos($angle)*$radius;
509 my $ey = $cy + sin($angle)*$radius;
510 my @line = line_from_points($cx, $cy, $ex, $ey);
511 #_test_line($cx, $cy, @line);
512 #_test_line($ex, $ey, @line);
513 my $goodsign = $line[0] * $tcx + $line[1] * $tcy + $line[2];
514 for my $pos (@entry{qw/left right/}) {
515 my $sign = $line[0] * ($pos+$tcx) + $line[1] * $y + $line[2];
516 if ($goodsign * $sign < 0) {
517 if (my @p = intersect_lines(@line, @{$entry{line}})) {
518 # die "$goodsign $sign ($pos, $tcx) no intersect (@line) (@{$entry{line}})" ; # this would be wierd
519 #_test_line(@p, @line);
520 #_test_line(@p, @{$entry{line}});
530 my $dist2 = ($pos+$tcx-$cx) * ($pos+$tcx-$cx)
531 + ($y - $cy) * ($y - $cy);
532 if ($dist2 > $radius * $radius) {
534 intersect_line_and_circle(@{$entry{line}}, $cx, $cy, $radius);
536 my @p = splice(@points, 0, 2);
537 if ($p[0] < $cx && $tcx+$pos < $p[0]) {
540 elsif ($p[0] > $cx && $tcx+$pos > $p[0]) {
547 push(@lines, \%entry);
549 my $left = $lines[0]{left} > $lines[1]{left} ? $lines[0]{left} : $lines[1]{left};
550 my $right = $lines[0]{right} < $lines[1]{right} ? $lines[0]{right} : $lines[1]{right};
551 return if $right - $left < $tbox[2];
553 return ($tcx+$left, $topy, $tcx+$right, $boty);
557 ( 'pie', $_[0]->SUPER::_composite() );
563 my %work = %{$self->SUPER::_style_defs()};
564 $work{otherlabel} = "(others)";
582 Tony Cook <tony@develop-help.com>
586 Imager::Graph(3), Imager(3), perl(1)